22 Commits 1e422b2c2b ... 1eb0de7938

Author SHA1 Message Date
  The MMGen Project 1eb0de7938 ETH: transaction sending via Etherscan 9 months ago
  The MMGen Project 7e5ee50d0b tx.new: minor fixes 9 months ago
  The MMGen Project 56f730ef7c proto.eth.rpc: remove Reth mainnet warning 9 months ago
  The MMGen Project 1f166ce458 mmgen-txsend: add --test option 9 months ago
  The MMGen Project 1cab2f9d6d tx.new: support relative fees < 1 unit; add test 9 months ago
  The MMGen Project c7b2626d6e amt.py: reimplement CoinAmt.to_unit(), add test 9 months ago
  The MMGen Project 2c3fa1c49a minor cleanups 9 months ago
  The MMGen Project 6967456f8f mmgen-txsend: add --dump-hex and --mark-sent options 9 months ago
  The MMGen Project 886f8e3029 make --daemon-id a coin-specific cfg file option 9 months ago
  The MMGen Project e972e4f93e add --list-daemon-ids global config opt 9 months ago
  The MMGen Project 81e9889766 add --test-suite global config opt 9 months ago
  The MMGen Project 8ad00b5053 main_txsend.py: cleanups 9 months ago
  The MMGen Project f305bdd491 cmdtest.py regtest: cleanups 9 months ago
  The MMGen Project 70022b04b2 use keyword-only parameters where practicable (103 files changed) 9 months ago
  The MMGen Project 168e6bbc56 whitespace, comments, minor cleanups 9 months ago
  The MMGen Project 1bbb1816e1 tw,tx: make parameters `width` and `iwidth` positional-only 9 months ago
  The MMGen Project 89ad0fd29b keyword-only parameters throughout 9 months ago
  The MMGen Project 0563916c96 Addrlist(): `addrfile` -> `infile` 9 months ago
  The MMGen Project a50e9383a9 minor whitespace 9 months ago
  The MMGen Project 915473cc2d test suite: minor fixes 9 months ago
  The MMGen Project ef2d8ae82d M test/daemontest_d/ut_rpc.py 9 months ago
  The MMGen Project 5252e8d691 test-release.py: restore Pylint test 9 months ago
180 changed files with 1605 additions and 772 deletions
  1. 26 16
      doc/wiki/commands/command-help-txsend.md
  2. 7 7
      mmgen/addr.py
  3. 4 4
      mmgen/addrdata.py
  4. 4 3
      mmgen/addrfile.py
  5. 10 7
      mmgen/addrlist.py
  6. 2 2
      mmgen/altcoin/params.py
  7. 1 1
      mmgen/altcoin/util.py
  8. 13 6
      mmgen/amt.py
  9. 13 13
      mmgen/autosign.py
  10. 7 7
      mmgen/baseconv.py
  11. 7 7
      mmgen/bip39.py
  12. 36 22
      mmgen/bip_hd/__init__.py
  13. 14 9
      mmgen/cfg.py
  14. 1 1
      mmgen/cfgfile.py
  15. 18 12
      mmgen/crypto.py
  16. 18 12
      mmgen/daemon.py
  17. 4 0
      mmgen/data/mmgen.cfg
  18. 1 1
      mmgen/data/version
  19. 1 1
      mmgen/devtools.py
  20. 8 8
      mmgen/filename.py
  21. 15 8
      mmgen/fileutil.py
  22. 9 0
      mmgen/help/__init__.py
  23. 5 1
      mmgen/help/help_notes.py
  24. 1 1
      mmgen/help/txsign.py
  25. 2 2
      mmgen/key.py
  26. 3 3
      mmgen/keygen.py
  27. 10 3
      mmgen/led.py
  28. 2 2
      mmgen/main_addrgen.py
  29. 2 2
      mmgen/main_addrimport.py
  30. 1 1
      mmgen/main_autosign.py
  31. 5 5
      mmgen/main_msg.py
  32. 2 2
      mmgen/main_passgen.py
  33. 1 1
      mmgen/main_seedjoin.py
  34. 7 4
      mmgen/main_tool.py
  35. 84 26
      mmgen/main_txsend.py
  36. 1 1
      mmgen/main_txsign.py
  37. 2 2
      mmgen/main_wallet.py
  38. 1 1
      mmgen/main_xmrwallet.py
  39. 4 4
      mmgen/mn_entry.py
  40. 5 5
      mmgen/msg.py
  41. 12 9
      mmgen/obj.py
  42. 10 8
      mmgen/objmethods.py
  43. 7 3
      mmgen/opts.py
  44. 9 12
      mmgen/passwdlist.py
  45. 3 3
      mmgen/platform/darwin/util.py
  46. 1 1
      mmgen/proto/btc/addrdata.py
  47. 2 1
      mmgen/proto/btc/params.py
  48. 6 6
      mmgen/proto/btc/regtest.py
  49. 7 2
      mmgen/proto/btc/rpc.py
  50. 1 1
      mmgen/proto/btc/tw/addresses.py
  51. 2 2
      mmgen/proto/btc/tw/bal.py
  52. 1 1
      mmgen/proto/btc/tw/ctl.py
  53. 1 1
      mmgen/proto/btc/tw/rpc.py
  54. 10 10
      mmgen/proto/btc/tw/txhistory.py
  55. 3 3
      mmgen/proto/btc/tx/base.py
  56. 0 4
      mmgen/proto/btc/tx/completed.py
  57. 12 12
      mmgen/proto/btc/tx/info.py
  58. 10 5
      mmgen/proto/btc/tx/new.py
  59. 23 2
      mmgen/proto/btc/tx/online.py
  60. 1 1
      mmgen/proto/btc/tx/op_return_data.py
  61. 1 1
      mmgen/proto/eth/addrdata.py
  62. 7 4
      mmgen/proto/eth/contract.py
  63. 3 3
      mmgen/proto/eth/daemon.py
  64. 1 1
      mmgen/proto/eth/misc.py
  65. 1 0
      mmgen/proto/eth/params.py
  66. 5 7
      mmgen/proto/eth/rpc.py
  67. 1 1
      mmgen/proto/eth/tw/addresses.py
  68. 2 2
      mmgen/proto/eth/tw/bal.py
  69. 7 6
      mmgen/proto/eth/tw/ctl.py
  70. 1 1
      mmgen/proto/eth/tw/json.py
  71. 2 2
      mmgen/proto/eth/tw/unspent.py
  72. 9 10
      mmgen/proto/eth/tx/base.py
  73. 1 1
      mmgen/proto/eth/tx/bump.py
  74. 3 3
      mmgen/proto/eth/tx/info.py
  75. 7 8
      mmgen/proto/eth/tx/new.py
  76. 4 1
      mmgen/proto/eth/tx/online.py
  77. 2 2
      mmgen/proto/secp256k1/keygen.py
  78. 1 0
      mmgen/proto/xmr/daemon.py
  79. 5 4
      mmgen/proto/xmr/rpc.py
  80. 4 2
      mmgen/protocol.py
  81. 10 9
      mmgen/rpc.py
  82. 2 2
      mmgen/seed.py
  83. 13 10
      mmgen/seedsplit.py
  84. 1 1
      mmgen/sha2.py
  85. 5 5
      mmgen/subseed.py
  86. 1 1
      mmgen/swap/proto/thorchain/memo.py
  87. 2 2
      mmgen/swap/proto/thorchain/thornode.py
  88. 8 8
      mmgen/term.py
  89. 1 1
      mmgen/tool/coin.py
  90. 1 1
      mmgen/tool/common.py
  91. 2 2
      mmgen/tool/file.py
  92. 8 8
      mmgen/tool/filecrypt.py
  93. 5 4
      mmgen/tool/fileutil.py
  94. 1 1
      mmgen/tool/help.py
  95. 4 2
      mmgen/tool/mnemonic.py
  96. 15 8
      mmgen/tool/rpc.py
  97. 16 12
      mmgen/tool/util.py
  98. 19 16
      mmgen/tool/wallet.py
  99. 14 13
      mmgen/tw/addresses.py
  100. 3 3
      mmgen/tw/bal.py
  101. 8 6
      mmgen/tw/ctl.py
  102. 5 2
      mmgen/tw/json.py
  103. 2 2
      mmgen/tw/shared.py
  104. 4 4
      mmgen/tw/txhistory.py
  105. 20 19
      mmgen/tw/unspent.py
  106. 6 5
      mmgen/tw/view.py
  107. 5 4
      mmgen/tx/base.py
  108. 9 9
      mmgen/tx/file.py
  109. 8 8
      mmgen/tx/info.py
  110. 25 19
      mmgen/tx/new.py
  111. 2 3
      mmgen/tx/online.py
  112. 6 6
      mmgen/tx/sign.py
  113. 223 0
      mmgen/tx/tx_proxy.py
  114. 11 10
      mmgen/ui.py
  115. 17 16
      mmgen/util.py
  116. 24 8
      mmgen/util2.py
  117. 3 0
      mmgen/wallet/__init__.py
  118. 5 5
      mmgen/wallet/base.py
  119. 1 1
      mmgen/wallet/brain.py
  120. 2 2
      mmgen/wallet/dieroll.py
  121. 3 3
      mmgen/wallet/enc.py
  122. 3 3
      mmgen/wallet/incog_base.py
  123. 5 5
      mmgen/wallet/mmgen.py
  124. 1 1
      mmgen/wallet/mmhex.py
  125. 6 6
      mmgen/wallet/mnemonic.py
  126. 1 1
      mmgen/wallet/plainhex.py
  127. 1 1
      mmgen/wallet/seed.py
  128. 2 2
      mmgen/xmrseed.py
  129. 1 1
      mmgen/xmrwallet/__init__.py
  130. 4 4
      mmgen/xmrwallet/file/__init__.py
  131. 5 5
      mmgen/xmrwallet/file/outputs.py
  132. 6 6
      mmgen/xmrwallet/file/tx.py
  133. 2 2
      mmgen/xmrwallet/include.py
  134. 2 2
      mmgen/xmrwallet/ops/__init__.py
  135. 1 1
      mmgen/xmrwallet/ops/create.py
  136. 1 1
      mmgen/xmrwallet/ops/import.py
  137. 1 1
      mmgen/xmrwallet/ops/label.py
  138. 1 1
      mmgen/xmrwallet/ops/relay.py
  139. 1 1
      mmgen/xmrwallet/ops/sign.py
  140. 1 1
      mmgen/xmrwallet/ops/txview.py
  141. 6 6
      mmgen/xmrwallet/ops/wallet.py
  142. 5 5
      mmgen/xmrwallet/rpc.py
  143. 1 0
      nix/packages.nix
  144. 9 0
      pyproject.toml
  145. 1 0
      setup.cfg
  146. 4 0
      test/cmdtest_d/common.py
  147. 14 4
      test/cmdtest_d/ct_automount.py
  148. 30 7
      test/cmdtest_d/ct_autosign.py
  149. 3 2
      test/cmdtest_d/ct_base.py
  150. 21 3
      test/cmdtest_d/ct_ethdev.py
  151. 3 3
      test/cmdtest_d/ct_input.py
  152. 11 5
      test/cmdtest_d/ct_main.py
  153. 97 21
      test/cmdtest_d/ct_regtest.py
  154. 17 7
      test/cmdtest_d/ct_shared.py
  155. 1 1
      test/cmdtest_d/ct_swap.py
  156. 1 1
      test/cmdtest_d/ct_xmr_autosign.py
  157. 3 11
      test/cmdtest_d/ct_xmrwallet.py
  158. 32 0
      test/cmdtest_d/etherscan.py
  159. 2 2
      test/daemontest_d/ut_exec.py
  160. 3 3
      test/daemontest_d/ut_msg.py
  161. 7 4
      test/daemontest_d/ut_rpc.py
  162. 1 1
      test/daemontest_d/ut_tx.py
  163. 3 3
      test/gentest.py
  164. 1 1
      test/include/common.py
  165. 2 2
      test/modtest_d/ut_addrlist.py
  166. 81 0
      test/modtest_d/ut_amt.py
  167. 1 1
      test/modtest_d/ut_bip39.py
  168. 1 1
      test/modtest_d/ut_bip_hd.py
  169. 1 1
      test/modtest_d/ut_gen.py
  170. 1 1
      test/modtest_d/ut_misc.py
  171. 6 6
      test/modtest_d/ut_seedsplit.py
  172. 8 8
      test/modtest_d/ut_subseed.py
  173. 2 2
      test/objattrtest_d/oat_btc_mainnet.py
  174. 16 9
      test/objtest_d/ot_btc_mainnet.py
  175. 1 1
      test/overlay/fakemods/mmgen/crypto.py
  176. 10 0
      test/overlay/fakemods/mmgen/tx/tx_proxy.py
  177. 34 0
      test/ref/ethereum/etherscan-form.html
  178. 33 0
      test/ref/ethereum/etherscan-result.html
  179. 16 2
      test/test-release.d/cfg.sh
  180. 6 3
      test/test-release.sh

+ 26 - 16
doc/wiki/commands/command-help-txsend.md

@@ -2,21 +2,31 @@
   MMGEN-TXSEND: Send a signed MMGen cryptocoin transaction
   MMGEN-TXSEND: Send a signed MMGen cryptocoin transaction
   USAGE:        mmgen-txsend [opts] [signed transaction file]
   USAGE:        mmgen-txsend [opts] [signed transaction file]
   OPTIONS:
   OPTIONS:
-  -h, --help      Print this help message
-      --longhelp  Print help message for long (global) 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
-  -q, --quiet     Suppress warnings; overwrite files without prompting
-  -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
-  -y, --yes       Answer 'yes' to prompts, suppress non-essential output
+  -h, --help       Print this help message
+      --longhelp   Print help message for long (global) 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
+  -H, --dump-hex F Instead of sending to the network, dump the transaction hex
+                   to file ‘F’.  Use filename ‘-’ to dump to standard output.
+  -m, --mark-sent  Mark the transaction as sent by adding it to the removable
+                   device.  Used in combination with --autosign when a trans-
+                   action has been successfully sent out-of-band.
+  -n, --tx-proxy P Send transaction via public TX proxy ‘P’ (supported proxies:
+                   ‘etherscan’).  This is done via a publicly accessible web
+                   page, so no API key or registration is required
+  -q, --quiet      Suppress warnings; overwrite files without prompting
+  -s, --status     Get status of a sent transaction (or current transaction,
+                   whether sent or unsent, when used with --autosign)
+  -t, --test       Test whether the transaction can be sent without sending it
+  -v, --verbose    Be more verbose
+  -x, --proxy P    Connect to TX proxy via SOCKS5 proxy ‘P’ (host:port)
+  -y, --yes        Answer 'yes' to prompts, suppress non-essential output
 
 
-  MMGEN v15.1.dev18              March 2025                    MMGEN-TXSEND(1)
+  MMGEN v15.1.dev20              March 2025                    MMGEN-TXSEND(1)
 ```
 ```

+ 7 - 7
mmgen/addr.py

@@ -54,7 +54,7 @@ class MMGenAddrType(HiliteStr, InitErrors, MMGenObject):
 		'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif',     ('viewkey',),      'Zcash z-address'),
 		'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif',     ('viewkey',),      'Zcash z-address'),
 		'M': ati('monero', 'monero', False,'monero',  'monero',  'spendkey',('viewkey','wallet_passwd'),'Monero address'),
 		'M': ati('monero', 'monero', False,'monero',  'monero',  'spendkey',('viewkey','wallet_passwd'),'Monero address'),
 	}
 	}
-	def __new__(cls, proto, id_str, errmsg=None):
+	def __new__(cls, proto, id_str, *, errmsg=None):
 		if isinstance(id_str, cls):
 		if isinstance(id_str, cls):
 			return id_str
 			return id_str
 		try:
 		try:
@@ -99,7 +99,7 @@ class AddrListID(HiliteStr, InitErrors, MMGenObject):
 	width = 10
 	width = 10
 	trunc_ok = False
 	trunc_ok = False
 	color = 'yellow'
 	color = 'yellow'
-	def __new__(cls, sid=None, mmtype=None, proto=None, id_str=None):
+	def __new__(cls, *, sid=None, mmtype=None, proto=None, id_str=None):
 		try:
 		try:
 			if id_str:
 			if id_str:
 				a, b = id_str.split(':')
 				a, b = id_str.split(':')
@@ -182,14 +182,14 @@ class CoinAddr(HiliteStr, InitErrors, MMGenObject):
 
 
 	# reimplement some HiliteStr methods:
 	# reimplement some HiliteStr methods:
 	@classmethod
 	@classmethod
-	def fmtc(cls, s, width, color=False):
-		return super().fmtc(s=s[:width-2]+'..' if len(s) > width else s, width=width, color=color)
+	def fmtc(cls, s, width, /, *, color=False):
+		return super().fmtc(s[:width-2]+'..' if len(s) > width else s, width, color=color)
 
 
-	def fmt(self, view_pref, width, color=False):
+	def fmt(self, view_pref, width, /, *, color=False):
 		s = self.views[view_pref]
 		s = self.views[view_pref]
-		return super().fmtc(f'{s[:width-2]}..' if len(s) > width else s, width=width, color=color)
+		return super().fmtc(f'{s[:width-2]}..' if len(s) > width else s, width, color=color)
 
 
-	def hl(self, view_pref, color=True):
+	def hl(self, view_pref, /, *, color=True):
 		return getattr(color_mod, self.color)(self.views[view_pref]) if color else self.views[view_pref]
 		return getattr(color_mod, self.color)(self.views[view_pref]) if color else self.views[view_pref]
 
 
 def is_coin_addr(proto, s):
 def is_coin_addr(proto, s):

+ 4 - 4
mmgen/addrdata.py

@@ -29,7 +29,7 @@ from .addrlist import AddrListEntry, AddrListData, AddrList
 
 
 class AddrData(MMGenObject):
 class AddrData(MMGenObject):
 
 
-	def __init__(self, proto, *args, **kwargs):
+	def __init__(self, proto):
 		self.al_ids = {}
 		self.al_ids = {}
 		self.proto = proto
 		self.proto = proto
 		self.rpc = None
 		self.rpc = None
@@ -68,10 +68,10 @@ class AddrData(MMGenObject):
 
 
 class TwAddrData(AddrData, metaclass=AsyncInit):
 class TwAddrData(AddrData, metaclass=AsyncInit):
 
 
-	def __new__(cls, cfg, proto, *args, **kwargs):
+	def __new__(cls, cfg, proto, *, twctl=None):
 		return MMGenObject.__new__(proto.base_proto_subclass(cls, 'addrdata'))
 		return MMGenObject.__new__(proto.base_proto_subclass(cls, 'addrdata'))
 
 
-	async def __init__(self, cfg, proto, twctl=None):
+	async def __init__(self, cfg, proto, *, twctl=None):
 		from .rpc import rpc_init
 		from .rpc import rpc_init
 		from .tw.shared import TwLabel
 		from .tw.shared import TwLabel
 		from .seed import SeedID
 		from .seed import SeedID
@@ -79,7 +79,7 @@ class TwAddrData(AddrData, metaclass=AsyncInit):
 		self.proto = proto
 		self.proto = proto
 		self.rpc = await rpc_init(cfg, proto)
 		self.rpc = await rpc_init(cfg, proto)
 		self.al_ids = {}
 		self.al_ids = {}
-		twd = await self.get_tw_data(twctl)
+		twd = await self.get_tw_data(twctl=twctl)
 		out, i = {}, 0
 		out, i = {}, 0
 		for acct, addr_array in twd:
 		for acct, addr_array in twd:
 			l = get_obj(TwLabel, proto=self.proto, text=acct, silent=True)
 			l = get_obj(TwLabel, proto=self.proto, text=acct, silent=True)

+ 4 - 3
mmgen/addrfile.py

@@ -66,6 +66,7 @@ class AddrFile(MMGenObject):
 	def write(
 	def write(
 			self,
 			self,
 			fn            = None,
 			fn            = None,
+			*,
 			binary        = False,
 			binary        = False,
 			desc          = None,
 			desc          = None,
 			ask_overwrite = True,
 			ask_overwrite = True,
@@ -92,7 +93,7 @@ class AddrFile(MMGenObject):
 		)
 		)
 		return self.parent.al_id.sid + (' ' if lbl_p2 else '') + lbl_p2
 		return self.parent.al_id.sid + (' ' if lbl_p2 else '') + lbl_p2
 
 
-	def format(self, add_comments=False):
+	def format(self, *, add_comments=False):
 		p = self.parent
 		p = self.parent
 		if p.gen_passwds and p.pw_fmt in ('bip39', 'xmrseed'):
 		if p.gen_passwds and p.pw_fmt in ('bip39', 'xmrseed'):
 			desc_pfx = f'{p.pw_fmt.upper()} '
 			desc_pfx = f'{p.pw_fmt.upper()} '
@@ -200,7 +201,7 @@ class AddrFile(MMGenObject):
 
 
 		return ret
 		return ret
 
 
-	def parse_file(self, fn, buf=[], exit_on_error=True):
+	def parse_file(self, fn, *, buf=[], exit_on_error=True):
 
 
 		def parse_addrfile_label(lbl):
 		def parse_addrfile_label(lbl):
 			"""
 			"""
@@ -249,7 +250,7 @@ class AddrFile(MMGenObject):
 		p = self.parent
 		p = self.parent
 
 
 		from .fileutil import get_lines_from_file
 		from .fileutil import get_lines_from_file
-		lines = get_lines_from_file(p.cfg, fn, p.desc+' data', trim_comments=True)
+		lines = get_lines_from_file(p.cfg, fn, desc=f'{p.desc} data', trim_comments=True)
 
 
 		try:
 		try:
 			assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})'
 			assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})'

+ 10 - 7
mmgen/addrlist.py

@@ -30,7 +30,7 @@ class AddrIdxList(tuple, InitErrors, MMGenObject):
 
 
 	max_len = 1000000
 	max_len = 1000000
 
 
-	def __new__(cls, fmt_str=None, idx_list=None, sep=','):
+	def __new__(cls, *, fmt_str=None, idx_list=None, sep=','):
 		try:
 		try:
 			if fmt_str:
 			if fmt_str:
 				def gen():
 				def gen():
@@ -103,7 +103,7 @@ class AddrListIDStr(HiliteStr):
 	color = 'green'
 	color = 'green'
 	trunc_ok = False
 	trunc_ok = False
 
 
-	def __new__(cls, addrlist, fmt_str=None):
+	def __new__(cls, addrlist, *, fmt_str=None):
 		idxs = [e.idx for e in addrlist.data]
 		idxs = [e.idx for e in addrlist.data]
 		prev = idxs[0]
 		prev = idxs[0]
 		ret = [prev]
 		ret = [prev]
@@ -159,7 +159,8 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 			self,
 			self,
 			cfg,
 			cfg,
 			proto,
 			proto,
-			addrfile  = '',
+			*,
+			infile    = '',
 			al_id     = '',
 			al_id     = '',
 			adata     = [],
 			adata     = [],
 			seed      = '',
 			seed      = '',
@@ -185,11 +186,13 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 		if seed and addr_idxs:   # data from seed + idxs
 		if seed and addr_idxs:   # data from seed + idxs
 			self.al_id = AddrListID(sid=seed.sid, mmtype=MMGenAddrType(proto, mmtype or proto.dfl_mmtype))
 			self.al_id = AddrListID(sid=seed.sid, mmtype=MMGenAddrType(proto, mmtype or proto.dfl_mmtype))
 			src = 'gen'
 			src = 'gen'
-			adata = self.generate(seed, addr_idxs if isinstance(addr_idxs, AddrIdxList) else AddrIdxList(addr_idxs))
+			adata = self.generate(
+				seed,
+				addr_idxs if isinstance(addr_idxs, AddrIdxList) else AddrIdxList(fmt_str=addr_idxs))
 			do_chksum = True
 			do_chksum = True
-		elif addrfile:           # data from MMGen address file
-			self.infile = addrfile
-			adata = self.file.parse_file(addrfile) # sets self.al_id
+		elif infile:             # data from MMGen address file
+			self.infile = infile
+			adata = self.file.parse_file(infile) # sets self.al_id
 			do_chksum = True
 			do_chksum = True
 		elif al_id and adata:    # data from tracking wallet
 		elif al_id and adata:    # data from tracking wallet
 			self.al_id = al_id
 			self.al_id = al_id

+ 2 - 2
mmgen/altcoin/params.py

@@ -267,7 +267,7 @@ class CoinInfo:
 			return None
 			return None
 		return cls.coin_constants[network][idx]
 		return cls.coin_constants[network][idx]
 
 
-def make_proto(e, testnet=False):
+def make_proto(e, *, testnet=False):
 
 
 	proto = ('X_' if e.name[0] in '0123456789' else '') + e.name + ('Testnet' if testnet else '')
 	proto = ('X_' if e.name[0] in '0123456789' else '') + e.name + ('Testnet' if testnet else '')
 
 
@@ -297,7 +297,7 @@ def make_proto(e, testnet=False):
 		)
 		)
 	)
 	)
 
 
-def init_genonly_altcoins(usr_coin=None, testnet=False):
+def init_genonly_altcoins(usr_coin=None, *, testnet=False):
 	"""
 	"""
 	Initialize altcoin protocol class or classes for current network.
 	Initialize altcoin protocol class or classes for current network.
 	If usr_coin is a core coin, initialization is skipped.
 	If usr_coin is a core coin, initialization is skipped.

+ 1 - 1
mmgen/altcoin/util.py

@@ -14,7 +14,7 @@ altcoin.util: various altcoin-related utilities
 
 
 from ..util import die
 from ..util import die
 
 
-def decrypt_keystore(data, passwd, mac_algo=None, mac_params={}):
+def decrypt_keystore(data, passwd, *, mac_algo=None, mac_params={}):
 	"""
 	"""
 	Decrypt the encrypted data in a cross-chain keystore
 	Decrypt the encrypted data in a cross-chain keystore
 	Returns the decrypted data as a bytestring
 	Returns the decrypted data as a bytestring

+ 13 - 6
mmgen/amt.py

@@ -40,7 +40,7 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class
 	max_amt  = None   # coin supply if known, otherwise None
 	max_amt  = None   # coin supply if known, otherwise None
 	units    = ()     # defined unit names, e.g. ('satoshi',...)
 	units    = ()     # defined unit names, e.g. ('satoshi',...)
 
 
-	def __new__(cls, num, from_unit=None, from_decimal=False):
+	def __new__(cls, num, *, from_unit=None, from_decimal=False):
 
 
 		if isinstance(num, CoinAmt):
 		if isinstance(num, CoinAmt):
 			raise TypeError(f'CoinAmt: {num} is instance of {cls.__name__}')
 			raise TypeError(f'CoinAmt: {num} is instance of {cls.__name__}')
@@ -65,13 +65,18 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class
 			return cls.init_fail(e, num)
 			return cls.init_fail(e, num)
 
 
 	def to_unit(self, unit):
 	def to_unit(self, unit):
-		return int(Decimal(self) // getattr(self, unit))
+		if (u := getattr(self, unit)) == self.atomic:
+			return int(Decimal(self) // u)
+		else:
+			return Decimal('{:0.{w}f}'.format(
+				self / u,
+				w = (u/self.atomic).as_tuple().exponent))
 
 
 	@classmethod
 	@classmethod
 	def fmtc(cls, *args, **kwargs):
 	def fmtc(cls, *args, **kwargs):
 		cls.method_not_implemented()
 		cls.method_not_implemented()
 
 
-	def fmt(self, color=False, iwidth=1, prec=None): # iwidth: width of the integer part
+	def fmt(self, iwidth=1, /, *, color=False, prec=None): # iwidth: width of the integer part
 		prec = prec or self.max_prec
 		prec = prec or self.max_prec
 		if '.' in (s := str(self)):
 		if '.' in (s := str(self)):
 			a, b = s.split('.', 1)
 			a, b = s.split('.', 1)
@@ -83,11 +88,11 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class
 				s.rjust(iwidth).ljust(iwidth+prec+1),
 				s.rjust(iwidth).ljust(iwidth+prec+1),
 				color = color)
 				color = color)
 
 
-	def hl(self, color=True):
+	def hl(self, *, color=True):
 		return self.colorize(str(self), color=color)
 		return self.colorize(str(self), color=color)
 
 
 	# fancy highlighting with coin unit, enclosure, formatting
 	# fancy highlighting with coin unit, enclosure, formatting
-	def hl2(self, color=True, unit=False, fs='{}', encl=''):
+	def hl2(self, *, color=True, unit=False, fs='{}', encl=''):
 		res = fs.format(self)
 		res = fs.format(self)
 		return (
 		return (
 			encl[:-1]
 			encl[:-1]
@@ -156,7 +161,7 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class
 	def __mod__(self, *args, **kwargs):
 	def __mod__(self, *args, **kwargs):
 		self.method_not_implemented()
 		self.method_not_implemented()
 
 
-def is_coin_amt(proto, num, from_unit=None, from_decimal=False):
+def is_coin_amt(proto, num, *, from_unit=None, from_decimal=False):
 	assert proto.coin_amt, 'proto.coin_amt is None!  Did you call init_proto() with ‘need_amt’?'
 	assert proto.coin_amt, 'proto.coin_amt is None!  Did you call init_proto() with ‘need_amt’?'
 	return get_obj(proto.coin_amt, num=num, from_unit=from_unit, from_decimal=from_decimal, silent=True, return_bool=True)
 	return get_obj(proto.coin_amt, num=num, from_unit=from_unit, from_decimal=from_decimal, silent=True, return_bool=True)
 
 
@@ -165,6 +170,7 @@ class BTCAmt(CoinAmt):
 	max_prec = 8
 	max_prec = 8
 	max_amt = 21000000
 	max_amt = 21000000
 	satoshi = Decimal('0.00000001')
 	satoshi = Decimal('0.00000001')
+	atomic = satoshi
 	units = ('satoshi',)
 	units = ('satoshi',)
 
 
 class BCHAmt(BTCAmt):
 class BCHAmt(BTCAmt):
@@ -190,6 +196,7 @@ class ETHAmt(CoinAmt):
 	Gwei    = Decimal('0.000000001')
 	Gwei    = Decimal('0.000000001')
 	szabo   = Decimal('0.000001')
 	szabo   = Decimal('0.000001')
 	finney  = Decimal('0.001')
 	finney  = Decimal('0.001')
+	atomic  = wei
 	units   = ('wei', 'Kwei', 'Mwei', 'Gwei', 'szabo', 'finney')
 	units   = ('wei', 'Kwei', 'Mwei', 'Gwei', 'szabo', 'finney')
 
 
 	def toWei(self):
 	def toWei(self):

+ 13 - 13
mmgen/autosign.py

@@ -33,12 +33,12 @@ def SwapMgr(*args, **kwargs):
 
 
 class SwapMgrBase:
 class SwapMgrBase:
 
 
-	def __init__(self, cfg, ignore_zram=False):
+	def __init__(self, cfg, *, ignore_zram=False):
 		self.cfg = cfg
 		self.cfg = cfg
 		self.ignore_zram = ignore_zram
 		self.ignore_zram = ignore_zram
 		self.desc = 'disk swap' if ignore_zram else 'swap'
 		self.desc = 'disk swap' if ignore_zram else 'swap'
 
 
-	def enable(self, quiet=False):
+	def enable(self, *, quiet=False):
 		ret = self.do_enable()
 		ret = self.do_enable()
 		if not quiet:
 		if not quiet:
 			self.cfg._util.qmsg(
 			self.cfg._util.qmsg(
@@ -47,7 +47,7 @@ class SwapMgrBase:
 				f'Could not enable {self.desc}')
 				f'Could not enable {self.desc}')
 		return ret
 		return ret
 
 
-	def disable(self, quiet=False):
+	def disable(self, *, quiet=False):
 		self.cfg._util.qmsg_r(f'Attempting to disable {self.desc}...')
 		self.cfg._util.qmsg_r(f'Attempting to disable {self.desc}...')
 		ret = self.do_disable()
 		ret = self.do_disable()
 		self.cfg._util.qmsg('success')
 		self.cfg._util.qmsg('success')
@@ -189,7 +189,7 @@ class Signable:
 				b = '  {}\n'.format('\n  '.join(self.gen_bad_list(sorted(bad_files, key=lambda f: f.name))))
 				b = '  {}\n'.format('\n  '.join(self.gen_bad_list(sorted(bad_files, key=lambda f: f.name))))
 			))
 			))
 
 
-		def die_wrong_num_txs(self, tx_type, msg=None, desc=None, show_dir=False):
+		def die_wrong_num_txs(self, tx_type, *, msg=None, desc=None, show_dir=False):
 			num_txs = len(getattr(self, tx_type))
 			num_txs = len(getattr(self, tx_type))
 			die('AutosignTXError', "{m}{a} {b} transaction{c} {d} {e}!".format(
 			die('AutosignTXError', "{m}{a} {b} transaction{c} {d} {e}!".format(
 				m = msg + '\n' if msg else '',
 				m = msg + '\n' if msg else '',
@@ -310,8 +310,8 @@ class Signable:
 					for tx, non_mmgen in body:
 					for tx, non_mmgen in body:
 						for nm in non_mmgen:
 						for nm in non_mmgen:
 							yield fs.format(
 							yield fs.format(
-								tx.txid.fmt(width=t_wid, color=True) if nm is non_mmgen[0] else ' '*t_wid,
-								nm.addr.fmt(nm.addr.view_pref, width=a_wid, color=True),
+								tx.txid.fmt(t_wid, color=True) if nm is non_mmgen[0] else ' '*t_wid,
+								nm.addr.fmt(nm.addr.view_pref, a_wid, color=True),
 								nm.amt.hl() + ' ' + yellow(tx.coin))
 								nm.amt.hl() + ' ' + yellow(tx.coin))
 
 
 				msg('\n' + '\n'.join(gen()))
 				msg('\n' + '\n'.join(gen()))
@@ -459,7 +459,7 @@ class Autosign:
 	def init_fixup(self): # see test/overlay/fakemods/mmgen/autosign.py
 	def init_fixup(self): # see test/overlay/fakemods/mmgen/autosign.py
 		pass
 		pass
 
 
-	def __init__(self, cfg, cmd=None):
+	def __init__(self, cfg, *, cmd=None):
 
 
 		if cfg.mnemonic_fmt:
 		if cfg.mnemonic_fmt:
 			if cfg.mnemonic_fmt not in self.mn_fmts:
 			if cfg.mnemonic_fmt not in self.mn_fmts:
@@ -581,7 +581,7 @@ class Autosign:
 
 
 		return self._wallet_files
 		return self._wallet_files
 
 
-	def do_mount(self, silent=False, verbose=False):
+	def do_mount(self, *, silent=False, verbose=False):
 
 
 		def check_or_create(dirname):
 		def check_or_create(dirname):
 			path = getattr(self, dirname)
 			path = getattr(self, dirname)
@@ -615,7 +615,7 @@ class Autosign:
 		for dirname in self.dirs:
 		for dirname in self.dirs:
 			check_or_create(dirname)
 			check_or_create(dirname)
 
 
-	def do_umount(self, silent=False, verbose=False):
+	def do_umount(self, *, silent=False, verbose=False):
 		if self.mountpoint.is_mount():
 		if self.mountpoint.is_mount():
 			run(['sync'], check=True)
 			run(['sync'], check=True)
 			if not silent:
 			if not silent:
@@ -630,7 +630,7 @@ class Autosign:
 		fails = 0
 		fails = 0
 		for wf in self.wallet_files:
 		for wf in self.wallet_files:
 			try:
 			try:
-				Wallet(self.cfg, wf, ignore_in_fmt=True, passwd_file=str(self.keyfile))
+				Wallet(self.cfg, fn=wf, ignore_in_fmt=True, passwd_file=str(self.keyfile))
 			except SystemExit as e:
 			except SystemExit as e:
 				if e.code != 0:
 				if e.code != 0:
 					fails += 1
 					fails += 1
@@ -710,7 +710,7 @@ class Autosign:
 			die(2, 'Unable to write ' + desc)
 			die(2, 'Unable to write ' + desc)
 		msg('Wrote ' + desc)
 		msg('Wrote ' + desc)
 
 
-	def gen_key(self, no_unmount=False):
+	def gen_key(self, *, no_unmount=False):
 		if not self.device_inserted:
 		if not self.device_inserted:
 			die(1, 'Removable device not present!')
 			die(1, 'Removable device not present!')
 		self.do_mount()
 		self.do_mount()
@@ -728,7 +728,7 @@ class Autosign:
 	def _get_macOS_ramdisk_size(self):
 	def _get_macOS_ramdisk_size(self):
 		from .platform.darwin.util import MacOSRamDisk, warn_ramdisk_too_small
 		from .platform.darwin.util import MacOSRamDisk, warn_ramdisk_too_small
 		# allow 1MB for each Monero wallet
 		# allow 1MB for each Monero wallet
-		xmr_size = len(AddrIdxList(self.cfg.xmrwallets)) if self.cfg.xmrwallets else 0
+		xmr_size = len(AddrIdxList(fmt_str=self.cfg.xmrwallets)) if self.cfg.xmrwallets else 0
 		calc_size = xmr_size + 1
 		calc_size = xmr_size + 1
 		usr_size = self.cfg.macos_ramdisk_size or self.cfg.macos_autosign_ramdisk_size
 		usr_size = self.cfg.macos_ramdisk_size or self.cfg.macos_autosign_ramdisk_size
 		if is_int(usr_size):
 		if is_int(usr_size):
@@ -776,7 +776,7 @@ class Autosign:
 				cfg         = self.cfg,
 				cfg         = self.cfg,
 				prompt      = f"Default wallet '{wf}' found.\nUse default wallet for autosigning?",
 				prompt      = f"Default wallet '{wf}' found.\nUse default wallet for autosigning?",
 				default_yes = True):
 				default_yes = True):
-			ss_in = Wallet(Config(), wf)
+			ss_in = Wallet(Config(), fn=wf)
 		else:
 		else:
 			ss_in = Wallet(self.cfg, in_fmt=self.mn_fmts[self.cfg.mnemonic_fmt or self.dfl_mn_fmt])
 			ss_in = Wallet(self.cfg, in_fmt=self.mn_fmts[self.cfg.mnemonic_fmt or self.dfl_mn_fmt])
 		ss_out = Wallet(self.cfg, ss=ss_in, passwd_file=str(self.keyfile))
 		ss_out = Wallet(self.cfg, ss=ss_in, passwd_file=str(self.keyfile))

+ 7 - 7
mmgen/baseconv.py

@@ -113,7 +113,7 @@ class baseconv:
 			die(3, 'ERROR: List is not sorted!')
 			die(3, 'ERROR: List is not sorted!')
 
 
 	@staticmethod
 	@staticmethod
-	def get_pad(pad, seed_pad_func):
+	def get_pad(pad, /, seed_pad_func):
 		"""
 		"""
 		'pad' argument to baseconv conversion methods must be either None, 'seed' or an integer.
 		'pad' argument to baseconv conversion methods must be either None, 'seed' or an integer.
 		If None, output of minimum (but never zero) length will be produced.
 		If None, output of minimum (but never zero) length will be produced.
@@ -129,11 +129,11 @@ class baseconv:
 		else:
 		else:
 			die('BaseConversionPadError', f"{pad!r}: illegal value for 'pad' (must be None, 'seed' or int)")
 			die('BaseConversionPadError', f"{pad!r}: illegal value for 'pad' (must be None, 'seed' or int)")
 
 
-	def tohex(self, words_arg, pad=None):
+	def tohex(self, words_arg, /, *, pad=None):
 		"convert string or list data of instance base to a hexadecimal string"
 		"convert string or list data of instance base to a hexadecimal string"
-		return self.tobytes(words_arg, pad//2 if type(pad) is int else pad).hex()
+		return self.tobytes(words_arg, pad=pad//2 if type(pad) is int else pad).hex()
 
 
-	def tobytes(self, words_arg, pad=None):
+	def tobytes(self, words_arg, /, *, pad=None):
 		"convert string or list data of instance base to byte string"
 		"convert string or list data of instance base to byte string"
 
 
 		words = words_arg if isinstance(words_arg, (list, tuple)) else tuple(words_arg.strip())
 		words = words_arg if isinstance(words_arg, (list, tuple)) else tuple(words_arg.strip())
@@ -163,7 +163,7 @@ class baseconv:
 		bl = ret.bit_length()
 		bl = ret.bit_length()
 		return ret.to_bytes(max(pad_val, bl//8+bool(bl%8)), 'big')
 		return ret.to_bytes(max(pad_val, bl//8+bool(bl%8)), 'big')
 
 
-	def fromhex(self, hexstr, pad=None, tostr=False):
+	def fromhex(self, hexstr, /, *, pad=None, tostr=False):
 		"convert a hexadecimal string to a list or string data of instance base"
 		"convert a hexadecimal string to a list or string data of instance base"
 
 
 		from .util import is_hex_str
 		from .util import is_hex_str
@@ -172,9 +172,9 @@ class baseconv:
 				('seed data' if pad == 'seed' else f'{hexstr!r}:') +
 				('seed data' if pad == 'seed' else f'{hexstr!r}:') +
 				' not a hexadecimal string')
 				' not a hexadecimal string')
 
 
-		return self.frombytes(bytes.fromhex(hexstr), pad, tostr)
+		return self.frombytes(bytes.fromhex(hexstr), pad=pad, tostr=tostr)
 
 
-	def frombytes(self, bytestr, pad=None, tostr=False):
+	def frombytes(self, bytestr, /, *, pad=None, tostr=False):
 		"convert byte string to list or string data of instance base"
 		"convert byte string to list or string data of instance base"
 
 
 		if not bytestr:
 		if not bytestr:

+ 7 - 7
mmgen/bip39.py

@@ -54,24 +54,24 @@ class bip39(baseconv):
 		self.wl_id = 'bip39'
 		self.wl_id = 'bip39'
 
 
 	@classmethod
 	@classmethod
-	def nwords2seedlen(cls, nwords, in_bytes=False, in_hex=False):
+	def nwords2seedlen(cls, nwords, /, *, in_bytes=False, in_hex=False):
 		for k, v in cls.constants.items():
 		for k, v in cls.constants.items():
 			if v.mn_len == nwords:
 			if v.mn_len == nwords:
 				return k//8 if in_bytes else k//4 if in_hex else k
 				return k//8 if in_bytes else k//4 if in_hex else k
 		die('MnemonicError', f'{nwords!r}: invalid word length for BIP39 mnemonic')
 		die('MnemonicError', f'{nwords!r}: invalid word length for BIP39 mnemonic')
 
 
 	@classmethod
 	@classmethod
-	def seedlen2nwords(cls, seed_len, in_bytes=False, in_hex=False):
+	def seedlen2nwords(cls, seed_len, /, *, in_bytes=False, in_hex=False):
 		seed_bits = seed_len * 8 if in_bytes else seed_len * 4 if in_hex else seed_len
 		seed_bits = seed_len * 8 if in_bytes else seed_len * 4 if in_hex else seed_len
 		try:
 		try:
 			return cls.constants[seed_bits].mn_len
 			return cls.constants[seed_bits].mn_len
 		except Exception as e:
 		except Exception as e:
 			raise ValueError(f'{seed_bits!r}: invalid seed length for BIP39 mnemonic') from e
 			raise ValueError(f'{seed_bits!r}: invalid seed length for BIP39 mnemonic') from e
 
 
-	def tohex(self, words_arg, pad=None):
+	def tohex(self, words_arg, /, *, pad=None):
 		return self.tobytes(words_arg, pad=pad).hex()
 		return self.tobytes(words_arg, pad=pad).hex()
 
 
-	def tobytes(self, words_arg, pad=None):
+	def tobytes(self, words_arg, /, *, pad=None):
 		assert isinstance(words_arg, (list, tuple)), 'words_arg must be list or tuple'
 		assert isinstance(words_arg, (list, tuple)), 'words_arg must be list or tuple'
 		assert pad in (None, 'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
 		assert pad in (None, 'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
 
 
@@ -105,11 +105,11 @@ class bip39(baseconv):
 
 
 		return seed_bytes
 		return seed_bytes
 
 
-	def fromhex(self, hexstr, pad=None, tostr=False):
+	def fromhex(self, hexstr, /, *, pad=None, tostr=False):
 		assert is_hex_str(hexstr), 'seed data not a hexadecimal string'
 		assert is_hex_str(hexstr), 'seed data not a hexadecimal string'
 		return self.frombytes(bytes.fromhex(hexstr), pad=pad, tostr=tostr)
 		return self.frombytes(bytes.fromhex(hexstr), pad=pad, tostr=tostr)
 
 
-	def frombytes(self, seed_bytes, pad=None, tostr=False):
+	def frombytes(self, seed_bytes, /, *, pad=None, tostr=False):
 		assert tostr is False, "'tostr' must be False for 'bip39'"
 		assert tostr is False, "'tostr' must be False for 'bip39'"
 		assert pad in (None, 'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
 		assert pad in (None, 'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
 
 
@@ -128,7 +128,7 @@ class bip39(baseconv):
 
 
 		return tuple(wl[int(res[i*11:(i+1)*11], 2)] for i in range(c.mn_len))
 		return tuple(wl[int(res[i*11:(i+1)*11], 2)] for i in range(c.mn_len))
 
 
-	def generate_seed(self, words_arg, passwd=''):
+	def generate_seed(self, words_arg, /, *, passwd=''):
 
 
 		self.tohex(words_arg) # validate
 		self.tohex(words_arg) # validate
 
 

+ 36 - 22
mmgen/bip_hd/__init__.py

@@ -137,7 +137,7 @@ class BipHDConfig(Lockable):
 
 
 	supported_coins = ('btc', 'eth', 'doge', 'ltc', 'bch')
 	supported_coins = ('btc', 'eth', 'doge', 'ltc', 'bch')
 
 
-	def __init__(self, base_cfg, coin, network, addr_type, from_path, no_path_checks):
+	def __init__(self, base_cfg, coin, *, network, addr_type, from_path, no_path_checks):
 
 
 		if not coin.lower() in self.supported_coins:
 		if not coin.lower() in self.supported_coins:
 			raise ValueError(f'bip_hd: coin {coin.upper()} not supported')
 			raise ValueError(f'bip_hd: coin {coin.upper()} not supported')
@@ -182,11 +182,24 @@ class MasterNode(Lockable):
 
 
 		check_privkey(int.from_bytes(self.key, byteorder='big'))
 		check_privkey(int.from_bytes(self.key, byteorder='big'))
 
 
-	def init_cfg(self, coin=None, network=None, addr_type=None, from_path=False, no_path_checks=False):
+	def init_cfg(
+			self,
+			coin           = None,
+			*,
+			network        = None,
+			addr_type      = None,
+			from_path      = False,
+			no_path_checks = False):
 
 
 		new = BipHDNodeMaster()
 		new = BipHDNodeMaster()
 
 
-		new.cfg       = BipHDConfig(self.base_cfg, coin, network, addr_type, from_path, no_path_checks)
+		new.cfg = BipHDConfig(
+			self.base_cfg,
+			coin,
+			network = network,
+			addr_type = addr_type,
+			from_path = from_path,
+			no_path_checks = no_path_checks)
 		new.par_print = self.par_print
 		new.par_print = self.par_print
 		new.depth     = self.depth
 		new.depth     = self.depth
 		new.key       = self.key
 		new.key       = self.key
@@ -198,11 +211,11 @@ class MasterNode(Lockable):
 		new._lock()
 		new._lock()
 		return new
 		return new
 
 
-	def to_coin_type(self, coin=None, network=None, addr_type=None):
-		return self.init_cfg(coin, network, addr_type).to_coin_type()
+	def to_coin_type(self, *, coin=None, network=None, addr_type=None):
+		return self.init_cfg(coin, network=network, addr_type=addr_type).to_coin_type()
 
 
-	def to_chain(self, idx, coin=None, network=None, addr_type=None, hardened=False, public=False):
-		return self.init_cfg(coin, network, addr_type).to_chain(
+	def to_chain(self, idx, *, coin=None, network=None, addr_type=None, hardened=False, public=False):
+		return self.init_cfg(coin, network=network, addr_type=addr_type).to_chain(
 			idx      = idx,
 			idx      = idx,
 			hardened = hardened,
 			hardened = hardened,
 			public   = public)
 			public   = public)
@@ -224,7 +237,7 @@ class BipHDNode(Lockable):
 					'None' if getattr(cls, name) is None else f'None or {getattr(cls, name)}')
 					'None' if getattr(cls, name) is None else f'None or {getattr(cls, name)}')
 			)
 			)
 
 
-	def set_params(self, cfg, idx, hardened):
+	def set_params(self, cfg, idx, *, hardened):
 		self.check_param('idx', idx)
 		self.check_param('idx', idx)
 		self.check_param('hardened', hardened)
 		self.check_param('hardened', hardened)
 		return (
 		return (
@@ -288,7 +301,7 @@ class BipHDNode(Lockable):
 	def xprv(self):
 	def xprv(self):
 		return self.key_extended(public=False, as_str=True)
 		return self.key_extended(public=False, as_str=True)
 
 
-	def key_extended(self, public, as_str=False):
+	def key_extended(self, public, *, as_str=False):
 		if self.public and not public:
 		if self.public and not public:
 			raise ValueError('cannot create extended private key for public node!')
 			raise ValueError('cannot create extended private key for public node!')
 		ret = b58chk_encode(
 		ret = b58chk_encode(
@@ -310,7 +323,7 @@ class BipHDNode(Lockable):
 	def derive_private(self, idx=None, hardened=None):
 	def derive_private(self, idx=None, hardened=None):
 		return self.derive(idx=idx, hardened=hardened, public=False)
 		return self.derive(idx=idx, hardened=hardened, public=False)
 
 
-	def derive(self, idx, hardened, public):
+	def derive(self, idx, *, hardened, public):
 
 
 		if self.public and not public:
 		if self.public and not public:
 			raise ValueError('cannot derive private node from public node!')
 			raise ValueError('cannot derive private node from public node!')
@@ -328,7 +341,7 @@ class BipHDNode(Lockable):
 			if new.public and type(new).hardened:
 			if new.public and type(new).hardened:
 				raise ValueError(
 				raise ValueError(
 					f'‘public’ requested, but node of depth {new.depth} ({new.desc}) must be hardened!')
 					f'‘public’ requested, but node of depth {new.depth} ({new.desc}) must be hardened!')
-			new.idx, new.hardened = new.set_params(new.cfg, idx, hardened)
+			new.idx, new.hardened = new.set_params(new.cfg, idx, hardened=hardened)
 
 
 		key_in = b'\x00' + self.key if new.hardened else self.pubkey_bytes
 		key_in = b'\x00' + self.key if new.hardened else self.pubkey_bytes
 
 
@@ -357,6 +370,7 @@ class BipHDNode(Lockable):
 			base_cfg,
 			base_cfg,
 			seed,
 			seed,
 			path_str,
 			path_str,
+			*,
 			coin           = None,
 			coin           = None,
 			addr_type      = None,
 			addr_type      = None,
 			no_path_checks = False):
 			no_path_checks = False):
@@ -384,14 +398,14 @@ class BipHDNode(Lockable):
 			if not is_int(idx):
 			if not is_int(idx):
 				raise ValueError(f'invalid path component {s!r}')
 				raise ValueError(f'invalid path component {s!r}')
 
 
-			res = res.derive(int(idx), hardened, public=False)
+			res = res.derive(int(idx), hardened=hardened, public=False)
 
 
 		return res
 		return res
 
 
 	@staticmethod
 	@staticmethod
 	# ‘addr_type’ is required for broken coins with duplicate version bytes across BIP protocols
 	# ‘addr_type’ is required for broken coins with duplicate version bytes across BIP protocols
 	# (i.e. Dogecoin)
 	# (i.e. Dogecoin)
-	def from_extended_key(base_cfg, coin, xkey_b58, addr_type=None):
+	def from_extended_key(base_cfg, coin, xkey_b58, *, addr_type=None):
 		xk = Bip32ExtendedKey(xkey_b58)
 		xk = Bip32ExtendedKey(xkey_b58)
 
 
 		if xk.public:
 		if xk.public:
@@ -410,10 +424,10 @@ class BipHDNode(Lockable):
 		new.cfg = BipHDConfig(
 		new.cfg = BipHDConfig(
 			base_cfg,
 			base_cfg,
 			coin,
 			coin,
-			xk.network,
-			addr_type or addr_types[xk.bip_proto],
-			False,
-			False)
+			network = xk.network,
+			addr_type = addr_type or addr_types[xk.bip_proto],
+			from_path = False,
+			no_path_checks = False)
 
 
 		new.par_print  = xk.par_print
 		new.par_print  = xk.par_print
 		new.depth      = xk.depth
 		new.depth      = xk.depth
@@ -435,7 +449,7 @@ class BipHDNodeMaster(BipHDNode):
 		#           purpose          coin_type
 		#           purpose          coin_type
 		return self.derive_private().derive_private()
 		return self.derive_private().derive_private()
 
 
-	def to_chain(self, idx, hardened=False, public=False):
+	def to_chain(self, idx, *, hardened=False, public=False):
 		#           purpose          coin_type        account #0            chain
 		#           purpose          coin_type        account #0            chain
 		return self.derive_private().derive_private().derive_private(idx=0).derive(
 		return self.derive_private().derive_private().derive_private(idx=0).derive(
 			idx      = idx,
 			idx      = idx,
@@ -446,7 +460,7 @@ class BipHDNodePurpose(BipHDNode):
 	desc = 'Purpose'
 	desc = 'Purpose'
 	hardened = True
 	hardened = True
 
 
-	def set_params(self, cfg, idx, hardened):
+	def set_params(self, cfg, idx, *, hardened):
 		self.check_param('hardened', hardened)
 		self.check_param('hardened', hardened)
 		if idx not in (None, cfg.bip_proto):
 		if idx not in (None, cfg.bip_proto):
 			raise ValueError(
 			raise ValueError(
@@ -458,7 +472,7 @@ class BipHDNodeCoinType(BipHDNode):
 	desc = 'Coin Type'
 	desc = 'Coin Type'
 	hardened = True
 	hardened = True
 
 
-	def set_params(self, cfg, idx, hardened):
+	def set_params(self, cfg, idx, *, hardened):
 		self.check_param('hardened', hardened)
 		self.check_param('hardened', hardened)
 		chain_idx = get_chain_params(
 		chain_idx = get_chain_params(
 			bipnum = get_bip_by_addr_type(cfg.addr_type),
 			bipnum = get_bip_by_addr_type(cfg.addr_type),
@@ -469,7 +483,7 @@ class BipHDNodeCoinType(BipHDNode):
 				f'chain index {chain_idx} for coin {cfg.base_cfg.coin!r}')
 				f'chain index {chain_idx} for coin {cfg.base_cfg.coin!r}')
 		return (chain_idx, type(self).hardened)
 		return (chain_idx, type(self).hardened)
 
 
-	def to_chain(self, idx, hardened=False, public=False):
+	def to_chain(self, idx, *, hardened=False, public=False):
 		#           account #0            chain
 		#           account #0            chain
 		return self.derive_private(idx=0).derive(
 		return self.derive_private(idx=0).derive(
 			idx      = idx,
 			idx      = idx,
@@ -484,7 +498,7 @@ class BipHDNodeChain(BipHDNode):
 	desc = 'Chain'
 	desc = 'Chain'
 	hardened = False
 	hardened = False
 
 
-	def set_params(self, cfg, idx, hardened):
+	def set_params(self, cfg, idx, *, hardened):
 		self.check_param('hardened', hardened)
 		self.check_param('hardened', hardened)
 		if idx not in (0, 1):
 		if idx not in (0, 1):
 			raise ValueError(
 			raise ValueError(

+ 14 - 9
mmgen/cfg.py

@@ -99,7 +99,7 @@ class GlobalConstants(Lockable):
 	else:
 	else:
 		die2(2, '$HOME is not set!  Unable to determine home directory')
 		die2(2, '$HOME is not set!  Unable to determine home directory')
 
 
-	def get_mmgen_data_file(self, filename, package='mmgen'):
+	def get_mmgen_data_file(self, *, filename, package='mmgen'):
 		"""
 		"""
 		this is an expensive import, so do only when required
 		this is an expensive import, so do only when required
 		"""
 		"""
@@ -292,6 +292,7 @@ class Config(Lockable):
 		'autosign',
 		'autosign',
 		'color',
 		'color',
 		'daemon_data_dir',
 		'daemon_data_dir',
+		'daemon_id', # also coin-specific
 		'debug',
 		'debug',
 		'fee_adjust',
 		'fee_adjust',
 		'force_256_color',
 		'force_256_color',
@@ -380,7 +381,9 @@ class Config(Lockable):
 		'fee_estimate_mode': _ov('nocase_pfx', ['conservative', 'economical']),
 		'fee_estimate_mode': _ov('nocase_pfx', ['conservative', 'economical']),
 		'rpc_backend':       _ov('nocase_pfx', ['auto', 'httplib', 'curl', 'aiohttp', 'requests']),
 		'rpc_backend':       _ov('nocase_pfx', ['auto', 'httplib', 'curl', 'aiohttp', 'requests']),
 		'swap_proto':        _ov('nocase_pfx', ['thorchain']),
 		'swap_proto':        _ov('nocase_pfx', ['thorchain']),
+		'tx_proxy':          _ov('nocase_pfx', ['etherscan']) # , 'blockchair'
 	}
 	}
+	_dfl_none_autoset_opts = ('tx_proxy',)
 
 
 	_auto_typeset_opts = {
 	_auto_typeset_opts = {
 		'seed_len': int,
 		'seed_len': int,
@@ -438,6 +441,7 @@ class Config(Lockable):
 	def __init__(
 	def __init__(
 			self,
 			self,
 			cfg          = None,
 			cfg          = None,
+			*,
 			opts_data    = None,
 			opts_data    = None,
 			init_opts    = None,
 			init_opts    = None,
 			parse_only   = False,
 			parse_only   = False,
@@ -521,7 +525,7 @@ class Config(Lockable):
 		# Step 4: set cfg from cfgfile, skipping already-set opts and auto opts; save set opts and auto
 		# Step 4: set cfg from cfgfile, skipping already-set opts and auto opts; save set opts and auto
 		#         opts to be set:
 		#         opts to be set:
 		# requires ‘data_dir_root’, ‘test_suite_cfgtest’
 		# requires ‘data_dir_root’, ‘test_suite_cfgtest’
-		self._cfgfile_opts = self._set_cfg_from_cfg_file(self._envopts, need_proto)
+		self._cfgfile_opts = self._set_cfg_from_cfg_file(self._envopts, need_proto=need_proto)
 
 
 		# Step 5: set autoset opts from user-supplied data, cfgfile data, or default values, in that order:
 		# Step 5: set autoset opts from user-supplied data, cfgfile data, or default values, in that order:
 		self._set_autoset_opts(self._cfgfile_opts.autoset)
 		self._set_autoset_opts(self._cfgfile_opts.autoset)
@@ -614,7 +618,7 @@ class Config(Lockable):
 			else:
 			else:
 				raise ValueError(f'{name!r} is not a valid MMGen environment variable')
 				raise ValueError(f'{name!r} is not a valid MMGen environment variable')
 
 
-	def _set_cfg_from_cfg_file(self, env_cfg, need_proto):
+	def _set_cfg_from_cfg_file(self, env_cfg, *, need_proto):
 
 
 		_ret = namedtuple('cfgfile_opts', ['non_auto', 'autoset', 'auto_typeset'])
 		_ret = namedtuple('cfgfile_opts', ['non_auto', 'autoset', 'auto_typeset'])
 
 
@@ -717,7 +721,8 @@ class Config(Lockable):
 				val = None
 				val = None
 
 
 			if val is None:
 			if val is None:
-				setattr(self, key, self._autoset_opts[key].choices[0])
+				if key not in self._dfl_none_autoset_opts:
+					setattr(self, key, self._autoset_opts[key].choices[0])
 			else:
 			else:
 				setattr(self, key, get_autoset_opt(key, val, src=src))
 				setattr(self, key, get_autoset_opt(key, val, src=src))
 
 
@@ -760,7 +765,7 @@ def check_opts(cfg): # Raises exception if any check fails
 			+ (f' in {cfg._cfgfile_fn!r}' if name in cfg._cfgfile_opts.non_auto else '')
 			+ (f' in {cfg._cfgfile_fn!r}' if name in cfg._cfgfile_opts.non_auto else '')
 		)
 		)
 
 
-	def display_opt(name, val='', beg='For selected', end=':\n'):
+	def display_opt(name, val='', *, beg='For selected', end=':\n'):
 		from .util import msg_r
 		from .util import msg_r
 		msg_r('{} option {!r}{}'.format(
 		msg_r('{} option {!r}{}'.format(
 			beg,
 			beg,
@@ -778,11 +783,11 @@ def check_opts(cfg): # Raises exception if any check fails
 		}[op_str](val, target):
 		}[op_str](val, target):
 			die('UserOptError', f'{val}: invalid {get_desc()} (not {op_str} {target})')
 			die('UserOptError', f'{val}: invalid {get_desc()} (not {op_str} {target})')
 
 
-	def opt_is_int(val, desc_pfx=''):
+	def opt_is_int(val, *, desc_pfx=''):
 		if not is_int(val):
 		if not is_int(val):
 			die('UserOptError', f'{val!r}: invalid {get_desc(desc_pfx)} (not an integer)')
 			die('UserOptError', f'{val!r}: invalid {get_desc(desc_pfx)} (not an integer)')
 
 
-	def opt_is_in_list(val, tlist, desc_pfx=''):
+	def opt_is_in_list(val, tlist, *, desc_pfx=''):
 		if val not in tlist:
 		if val not in tlist:
 			q, sep = (('', ','), ("'", "','"))[isinstance(tlist[0], str)]
 			q, sep = (('', ','), ("'", "','"))[isinstance(tlist[0], str)]
 			die('UserOptError', '{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}'.format(
 			die('UserOptError', '{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}'.format(
@@ -845,7 +850,7 @@ def check_opts(cfg): # Raises exception if any check fails
 			if hasattr(cfg, key2):
 			if hasattr(cfg, key2):
 				val2 = getattr(cfg, key2)
 				val2 = getattr(cfg, key2)
 				from .wallet import get_wallet_data
 				from .wallet import get_wallet_data
-				wd = get_wallet_data('incog_hidden')
+				wd = get_wallet_data(wtype='incog_hidden')
 				if val2 and val2 not in wd.fmt_codes:
 				if val2 and val2 not in wd.fmt_codes:
 					die('UserOptError', f'Option conflict:\n  {fmt_opt(name)}, with\n  {fmt_opt(key2)}={val2}')
 					die('UserOptError', f'Option conflict:\n  {fmt_opt(name)}, with\n  {fmt_opt(key2)}={val2}')
 
 
@@ -929,7 +934,7 @@ def opt_postproc_debug(cfg):
 	Msg('        {}\n'.format('\n        '.join(none_opts)))
 	Msg('        {}\n'.format('\n        '.join(none_opts)))
 	Msg('\n=== end opts.py debug ===\n')
 	Msg('\n=== end opts.py debug ===\n')
 
 
-def conv_type(name, val, refval, src, invert_bool=False):
+def conv_type(name, val, refval, *, src, invert_bool=False):
 
 
 	def do_fail():
 	def do_fail():
 		desc = {
 		desc = {

+ 1 - 1
mmgen/cfgfile.py

@@ -190,7 +190,7 @@ class CfgFileSampleSys(cfg_file_sample):
 		else:
 		else:
 			# self.fn is used for error msgs only, so file need not exist on filesystem
 			# self.fn is used for error msgs only, so file need not exist on filesystem
 			self.fn = os.path.join(os.path.dirname(__file__), 'data', self.fn_base)
 			self.fn = os.path.join(os.path.dirname(__file__), 'data', self.fn_base)
-			self.data = gc.get_mmgen_data_file(self.fn_base).splitlines()
+			self.data = gc.get_mmgen_data_file(filename=self.fn_base).splitlines()
 
 
 	def make_metadata(self):
 	def make_metadata(self):
 		return [f'# Version {self.cur_ver} {self.computed_chksum}']
 		return [f'# Version {self.cur_ver} {self.computed_chksum}']

+ 18 - 12
mmgen/crypto.py

@@ -89,10 +89,10 @@ class Crypto:
 			msg(f'Seed:  {seed.hex()!r}\nScramble key: {scramble_key}\nScrambled seed: {step1.hex()}\n')
 			msg(f'Seed:  {seed.hex()!r}\nScramble key: {scramble_key}\nScrambled seed: {step1.hex()}\n')
 		return self.sha256_rounds(step1)
 		return self.sha256_rounds(step1)
 
 
-	def encrypt_seed(self, data, key, desc='seed'):
-		return self.encrypt_data(data, key, desc=desc)
+	def encrypt_seed(self, data, key, *, desc='seed'):
+		return self.encrypt_data(data, key=key, desc=desc)
 
 
-	def decrypt_seed(self, enc_seed, key, seed_id, key_id):
+	def decrypt_seed(self, enc_seed, key, *, seed_id, key_id):
 		self.util.vmsg_r('Checking key...')
 		self.util.vmsg_r('Checking key...')
 		chk1 = make_chksum_8(key)
 		chk1 = make_chksum_8(key)
 		if key_id:
 		if key_id:
@@ -121,6 +121,7 @@ class Crypto:
 	def encrypt_data(
 	def encrypt_data(
 			self,
 			self,
 			data,
 			data,
+			*,
 			key,
 			key,
 			iv     = aesctr_dfl_iv,
 			iv     = aesctr_dfl_iv,
 			desc   = 'data',
 			desc   = 'data',
@@ -151,6 +152,7 @@ class Crypto:
 			self,
 			self,
 			enc_data,
 			enc_data,
 			key,
 			key,
+			*,
 			iv   = aesctr_dfl_iv,
 			iv   = aesctr_dfl_iv,
 			desc = 'data'):
 			desc = 'data'):
 
 
@@ -166,6 +168,7 @@ class Crypto:
 			passwd,
 			passwd,
 			salt,
 			salt,
 			hash_preset,
 			hash_preset,
+			*,
 			buflen = 32):
 			buflen = 32):
 
 
 		# Buflen arg is for brainwallets only, which use this function to generate
 		# Buflen arg is for brainwallets only, which use this function to generate
@@ -214,6 +217,7 @@ class Crypto:
 			passwd,
 			passwd,
 			salt,
 			salt,
 			hash_preset,
 			hash_preset,
+			*,
 			desc      = 'encryption key',
 			desc      = 'encryption key',
 			from_what = 'passphrase',
 			from_what = 'passphrase',
 			verbose   = False):
 			verbose   = False):
@@ -226,7 +230,7 @@ class Crypto:
 		self.util.dmsg(f'Key: {key.hex()}')
 		self.util.dmsg(f'Key: {key.hex()}')
 		return key
 		return key
 
 
-	def _get_random_data_from_user(self, uchars=None, desc='data'):
+	def _get_random_data_from_user(self, uchars=None, *, desc='data'):
 
 
 		if uchars is None:
 		if uchars is None:
 			uchars = self.cfg.usr_randchars
 			uchars = self.cfg.usr_randchars
@@ -295,6 +299,7 @@ class Crypto:
 	def add_user_random(
 	def add_user_random(
 			self,
 			self,
 			rand_bytes,
 			rand_bytes,
+			*,
 			desc,
 			desc,
 			urand = {'data':b'', 'counter':0}):
 			urand = {'data':b'', 'counter':0}):
 
 
@@ -327,6 +332,7 @@ class Crypto:
 	def get_hash_preset_from_user(
 	def get_hash_preset_from_user(
 			self,
 			self,
 			old_preset = gc.dfl_hash_preset,
 			old_preset = gc.dfl_hash_preset,
+			*,
 			data_desc  = 'data',
 			data_desc  = 'data',
 			prompt     = None):
 			prompt     = None):
 
 
@@ -345,7 +351,7 @@ class Crypto:
 			else:
 			else:
 				return old_preset
 				return old_preset
 
 
-	def get_new_passphrase(self, data_desc, hash_preset, passwd_file, pw_desc='passphrase'):
+	def get_new_passphrase(self, data_desc, hash_preset, passwd_file, *, pw_desc='passphrase'):
 		message = f"""
 		message = f"""
 				You must choose a passphrase to encrypt your {data_desc} with.
 				You must choose a passphrase to encrypt your {data_desc} with.
 				A key will be generated from your passphrase using a hash preset of '{hash_preset}'.
 				A key will be generated from your passphrase using a hash preset of '{hash_preset}'.
@@ -381,7 +387,7 @@ class Crypto:
 
 
 		return pw
 		return pw
 
 
-	def get_passphrase(self, data_desc, passwd_file, pw_desc='passphrase'):
+	def get_passphrase(self, data_desc, passwd_file, *, pw_desc='passphrase'):
 		if passwd_file:
 		if passwd_file:
 			from .fileutil import get_words_from_file
 			from .fileutil import get_words_from_file
 			return ' '.join(get_words_from_file(
 			return ' '.join(get_words_from_file(
@@ -393,7 +399,7 @@ class Crypto:
 			from .ui import get_words_from_user
 			from .ui import get_words_from_user
 			return ' '.join(get_words_from_user(self.cfg, f'Enter {pw_desc} for {data_desc}: '))
 			return ' '.join(get_words_from_user(self.cfg, f'Enter {pw_desc} for {data_desc}: '))
 
 
-	def mmgen_encrypt(self, data, desc='data', hash_preset=None):
+	def mmgen_encrypt(self, data, *, desc='data', hash_preset=None):
 		salt  = self.get_random(self.mmenc_salt_len)
 		salt  = self.get_random(self.mmenc_salt_len)
 		iv    = self.get_random(self.aesctr_iv_len)
 		iv    = self.get_random(self.aesctr_iv_len)
 		nonce = self.get_random(self.mmenc_nonce_len)
 		nonce = self.get_random(self.mmenc_nonce_len)
@@ -407,10 +413,10 @@ class Crypto:
 			passwd_file = self.cfg.passwd_file)
 			passwd_file = self.cfg.passwd_file)
 		key    = self.make_key(passwd, salt, hp)
 		key    = self.make_key(passwd, salt, hp)
 		from hashlib import sha256
 		from hashlib import sha256
-		enc_d  = self.encrypt_data(sha256(nonce+data).digest() + nonce + data, key, iv, desc=desc)
+		enc_d  = self.encrypt_data(sha256(nonce+data).digest() + nonce + data, key=key, iv=iv, desc=desc)
 		return salt+iv+enc_d
 		return salt+iv+enc_d
 
 
-	def mmgen_decrypt(self, data, desc='data', hash_preset=None):
+	def mmgen_decrypt(self, data, *, desc='data', hash_preset=None):
 		self.util.vmsg(f'Preparing to decrypt {desc}')
 		self.util.vmsg(f'Preparing to decrypt {desc}')
 		dstart = self.mmenc_salt_len + self.aesctr_iv_len
 		dstart = self.mmenc_salt_len + self.aesctr_iv_len
 		salt   = data[:self.mmenc_salt_len]
 		salt   = data[:self.mmenc_salt_len]
@@ -423,7 +429,7 @@ class Crypto:
 			data_desc = desc,
 			data_desc = desc,
 			passwd_file = self.cfg.passwd_file)
 			passwd_file = self.cfg.passwd_file)
 		key    = self.make_key(passwd, salt, hp)
 		key    = self.make_key(passwd, salt, hp)
-		dec_d  = self.decrypt_data(enc_d, key, iv, desc)
+		dec_d  = self.decrypt_data(enc_d, key, iv=iv, desc=desc)
 		sha256_len = 32
 		sha256_len = 32
 		from hashlib import sha256
 		from hashlib import sha256
 		if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
 		if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
@@ -433,9 +439,9 @@ class Crypto:
 			msg('Incorrect passphrase or hash preset')
 			msg('Incorrect passphrase or hash preset')
 			return False
 			return False
 
 
-	def mmgen_decrypt_retry(self, d, desc='data'):
+	def mmgen_decrypt_retry(self, d, *, desc='data'):
 		while True:
 		while True:
-			d_dec = self.mmgen_decrypt(d, desc)
+			d_dec = self.mmgen_decrypt(d, desc=desc)
 			if d_dec:
 			if d_dec:
 				return d_dec
 				return d_dec
 			msg('Trying again...')
 			msg('Trying again...')

+ 18 - 12
mmgen/daemon.py

@@ -50,7 +50,7 @@ class Daemon(Lockable):
 	_reset_ok = ('debug', 'wait', 'pids')
 	_reset_ok = ('debug', 'wait', 'pids')
 	version_info_arg = '--version'
 	version_info_arg = '--version'
 
 
-	def __init__(self, cfg, opts=None, flags=None):
+	def __init__(self, cfg, *, opts=None, flags=None):
 
 
 		self.cfg = cfg
 		self.cfg = cfg
 		self.platform = sys.platform
 		self.platform = sys.platform
@@ -78,7 +78,7 @@ class Daemon(Lockable):
 		p = Popen(cmd, creationflags=CREATE_NEW_CONSOLE, startupinfo=si)
 		p = Popen(cmd, creationflags=CREATE_NEW_CONSOLE, startupinfo=si)
 		p.wait()
 		p.wait()
 
 
-	def exec_cmd(self, cmd, is_daemon=False, check_retcode=False):
+	def exec_cmd(self, cmd, *, is_daemon=False, check_retcode=False):
 		out = (PIPE, None)[is_daemon and self.opt.no_daemonize]
 		out = (PIPE, None)[is_daemon and self.opt.no_daemonize]
 		try:
 		try:
 			cp = run(cmd, check=False, stdout=out, stderr=out)
 			cp = run(cmd, check=False, stdout=out, stderr=out)
@@ -91,7 +91,7 @@ class Daemon(Lockable):
 			print(cp)
 			print(cp)
 		return cp
 		return cp
 
 
-	def run_cmd(self, cmd, silent=False, is_daemon=False, check_retcode=False):
+	def run_cmd(self, cmd, *, silent=False, is_daemon=False, check_retcode=False):
 
 
 		if self.debug:
 		if self.debug:
 			msg('\n\n')
 			msg('\n\n')
@@ -105,7 +105,7 @@ class Daemon(Lockable):
 		if self.use_threads and is_daemon and not self.opt.no_daemonize:
 		if self.use_threads and is_daemon and not self.opt.no_daemonize:
 			ret = self.exec_cmd_thread(cmd)
 			ret = self.exec_cmd_thread(cmd)
 		else:
 		else:
-			ret = self.exec_cmd(cmd, is_daemon, check_retcode)
+			ret = self.exec_cmd(cmd, is_daemon=is_daemon, check_retcode=check_retcode)
 
 
 		if isinstance(ret, CompletedProcess):
 		if isinstance(ret, CompletedProcess):
 			if ret.stdout and (self.debug or not silent):
 			if ret.stdout and (self.debug or not silent):
@@ -166,7 +166,7 @@ class Daemon(Lockable):
 	def cli(self, *cmds, silent=False):
 	def cli(self, *cmds, silent=False):
 		return self.run_cmd(self.cli_cmd(*cmds), silent=silent)
 		return self.run_cmd(self.cli_cmd(*cmds), silent=silent)
 
 
-	def state_msg(self, extra_text=None):
+	def state_msg(self, *, extra_text=None):
 		try:
 		try:
 			pid = self.pid
 			pid = self.pid
 		except:
 		except:
@@ -181,7 +181,7 @@ class Daemon(Lockable):
 	def pre_start(self):
 	def pre_start(self):
 		pass
 		pass
 
 
-	def start(self, quiet=False, silent=False):
+	def start(self, *, quiet=False, silent=False):
 		if self.state == 'ready':
 		if self.state == 'ready':
 			if not (quiet or silent):
 			if not (quiet or silent):
 				msg(self.state_msg(extra_text='already'))
 				msg(self.state_msg(extra_text='already'))
@@ -200,7 +200,7 @@ class Daemon(Lockable):
 
 
 		return ret
 		return ret
 
 
-	def stop(self, quiet=False, silent=False):
+	def stop(self, *, quiet=False, silent=False):
 		if self.state == 'ready':
 		if self.state == 'ready':
 			if not silent:
 			if not silent:
 				msg(f'Stopping {self.desc} on port {self.bind_port}')
 				msg(f'Stopping {self.desc} on port {self.bind_port}')
@@ -221,11 +221,11 @@ class Daemon(Lockable):
 				msg(f'{self.desc} on port {self.bind_port} not running')
 				msg(f'{self.desc} on port {self.bind_port} not running')
 			return True
 			return True
 
 
-	def restart(self, silent=False):
+	def restart(self, *, silent=False):
 		self.stop(silent=silent)
 		self.stop(silent=silent)
 		return self.start(silent=silent)
 		return self.start(silent=silent)
 
 
-	def test_socket(self, host, port, timeout=10):
+	def test_socket(self, host, port, *, timeout=10):
 		import socket
 		import socket
 		try:
 		try:
 			socket.create_connection((host, port), timeout=timeout).close()
 			socket.create_connection((host, port), timeout=timeout).close()
@@ -258,7 +258,7 @@ class RPCDaemon(Daemon):
 
 
 	avail_opts = ('no_daemonize',)
 	avail_opts = ('no_daemonize',)
 
 
-	def __init__(self, cfg, opts=None, flags=None):
+	def __init__(self, cfg, *, opts=None, flags=None):
 		super().__init__(cfg, opts=opts, flags=flags)
 		super().__init__(cfg, opts=opts, flags=flags)
 		self.desc = '{} {} {}RPC daemon'.format(
 		self.desc = '{} {} {}RPC daemon'.format(
 			self.rpc_type,
 			self.rpc_type,
@@ -317,7 +317,7 @@ class CoinDaemon(Daemon):
 		return ret
 		return ret
 
 
 	@classmethod
 	@classmethod
-	def get_daemon(cls, cfg, coin, daemon_id, proto=None):
+	def get_daemon(cls, cfg, coin, daemon_id, *, proto=None):
 		if proto:
 		if proto:
 			proto_cls = type(proto)
 			proto_cls = type(proto)
 		else:
 		else:
@@ -339,6 +339,7 @@ class CoinDaemon(Daemon):
 
 
 	def __new__(cls,
 	def __new__(cls,
 			cfg,
 			cfg,
+			*,
 			network_id = None,
 			network_id = None,
 			proto      = None,
 			proto      = None,
 			opts       = None,
 			opts       = None,
@@ -366,7 +367,11 @@ class CoinDaemon(Daemon):
 		daemon_ids = cls.get_daemon_ids(cfg, coin)
 		daemon_ids = cls.get_daemon_ids(cfg, coin)
 		if not daemon_ids:
 		if not daemon_ids:
 			die(1, f'No configured daemons for coin {coin}!')
 			die(1, f'No configured daemons for coin {coin}!')
-		daemon_id = daemon_id or cfg.daemon_id or daemon_ids[0]
+		daemon_id = (
+			daemon_id
+			or getattr(cfg, f'{coin.lower()}_daemon_id', None)
+			or cfg.daemon_id
+			or daemon_ids[0])
 
 
 		if daemon_id not in daemon_ids:
 		if daemon_id not in daemon_ids:
 			die(1, f'{daemon_id!r}: invalid daemon_id - valid choices: {fmt_list(daemon_ids)}')
 			die(1, f'{daemon_id!r}: invalid daemon_id - valid choices: {fmt_list(daemon_ids)}')
@@ -384,6 +389,7 @@ class CoinDaemon(Daemon):
 
 
 	def __init__(self,
 	def __init__(self,
 			cfg,
 			cfg,
+			*,
 			network_id = None,
 			network_id = None,
 			proto      = None,
 			proto      = None,
 			opts       = None,
 			opts       = None,

+ 4 - 0
mmgen/data/mmgen.cfg

@@ -77,6 +77,10 @@
 # setups with unusually large Monero wallets:
 # setups with unusually large Monero wallets:
 # macos_autosign_ramdisk_size 10
 # macos_autosign_ramdisk_size 10
 
 
+# Specify the daemon ID.  This option also has coin-specific variants (see
+# below):
+# daemon_id bitcoin_core
+
 # Ignore coin daemon version. This option also has coin-specific variants
 # Ignore coin daemon version. This option also has coin-specific variants
 # (see below):
 # (see below):
 # ignore_daemon_version false
 # ignore_daemon_version false

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev19
+15.1.dev20

+ 1 - 1
mmgen/devtools.py

@@ -131,7 +131,7 @@ class MMGenObjectMethods: # mixin class for MMGenObject
 		def isScalar(obj):
 		def isScalar(obj):
 			return isinstance(obj, scalars)
 			return isinstance(obj, scalars)
 
 
-		def do_list(out, e, lvl=0, is_dict=False):
+		def do_list(out, e, *, lvl=0, is_dict=False):
 			out.append('\n')
 			out.append('\n')
 			for i in e:
 			for i in e:
 				el = i if not is_dict else e[i]
 				el = i if not is_dict else e[i]

+ 8 - 8
mmgen/filename.py

@@ -25,7 +25,7 @@ from .util import die, get_extension
 
 
 class File:
 class File:
 
 
-	def __init__(self, fn, write=False):
+	def __init__(self, fn, *, write=False):
 
 
 		self.name     = fn
 		self.name     = fn
 		self.dirname  = os.path.dirname(fn)
 		self.dirname  = os.path.dirname(fn)
@@ -66,21 +66,21 @@ class File:
 
 
 class FileList(list):
 class FileList(list):
 
 
-	def __init__(self, fns, write=False):
+	def __init__(self, fns, *, write=False):
 		list.__init__(
 		list.__init__(
 			self,
 			self,
-			[File(fn, write) for fn in fns])
+			[File(fn, write=write) for fn in fns])
 
 
 	def names(self):
 	def names(self):
 		return [f.name for f in self]
 		return [f.name for f in self]
 
 
-	def sort_by_age(self, key='mtime', reverse=False):
+	def sort_by_age(self, *, key='mtime', reverse=False):
 		assert key in ('atime', 'ctime', 'mtime'), f'{key!r}: invalid sort key'
 		assert key in ('atime', 'ctime', 'mtime'), f'{key!r}: invalid sort key'
 		self.sort(key=lambda a: getattr(a, key), reverse=reverse)
 		self.sort(key=lambda a: getattr(a, key), reverse=reverse)
 
 
 class MMGenFile(File):
 class MMGenFile(File):
 
 
-	def __init__(self, fn, base_class=None, subclass=None, proto=None, write=False):
+	def __init__(self, fn, *, base_class=None, subclass=None, proto=None, write=False):
 		"""
 		"""
 		'base_class' - a base class with an 'ext_to_cls' method
 		'base_class' - a base class with an 'ext_to_cls' method
 		'subclass'   - a subclass with an 'ext' attribute
 		'subclass'   - a subclass with an 'ext' attribute
@@ -91,7 +91,7 @@ class MMGenFile(File):
 		attribute to True.
 		attribute to True.
 		"""
 		"""
 
 
-		super().__init__(fn, write)
+		super().__init__(fn, write=write)
 
 
 		assert (subclass or base_class) and not (subclass and base_class), 'MMGenFile chk1'
 		assert (subclass or base_class) and not (subclass and base_class), 'MMGenFile chk1'
 
 
@@ -107,12 +107,12 @@ class MMGenFile(File):
 
 
 class MMGenFileList(FileList):
 class MMGenFileList(FileList):
 
 
-	def __init__(self, fns, base_class, proto=None, write=False):
+	def __init__(self, fns, base_class, *, proto=None, write=False):
 		list.__init__(
 		list.__init__(
 			self,
 			self,
 			[MMGenFile(fn, base_class=base_class, proto=proto, write=write) for fn in fns])
 			[MMGenFile(fn, base_class=base_class, proto=proto, write=write) for fn in fns])
 
 
-def find_files_in_dir(subclass, fdir, no_dups=False):
+def find_files_in_dir(subclass, fdir, *, no_dups=False):
 
 
 	assert isinstance(subclass, type), f'{subclass}: not a class'
 	assert isinstance(subclass, type), f'{subclass}: not a class'
 
 

+ 15 - 8
mmgen/fileutil.py

@@ -57,7 +57,7 @@ def check_binary(args):
 		die(2, f'{args[0]!r} binary missing, not in path, or not executable')
 		die(2, f'{args[0]!r} binary missing, not in path, or not executable')
 	set_vt100()
 	set_vt100()
 
 
-def shred_file(fn, verbose=False):
+def shred_file(fn, *, verbose=False):
 	check_binary(['shred', '--version'])
 	check_binary(['shred', '--version'])
 	from subprocess import run
 	from subprocess import run
 	run(
 	run(
@@ -67,7 +67,7 @@ def shred_file(fn, verbose=False):
 		check=True)
 		check=True)
 	set_vt100()
 	set_vt100()
 
 
-def _check_file_type_and_access(fname, ftype, blkdev_ok=False):
+def _check_file_type_and_access(fname, ftype, *, blkdev_ok=False):
 
 
 	import stat
 	import stat
 
 
@@ -103,16 +103,20 @@ def _check_file_type_and_access(fname, ftype, blkdev_ok=False):
 
 
 	return True
 	return True
 
 
-def check_infile(f, blkdev_ok=False):
+def check_infile(f, *, blkdev_ok=False):
 	return _check_file_type_and_access(f, 'input file', blkdev_ok=blkdev_ok)
 	return _check_file_type_and_access(f, 'input file', blkdev_ok=blkdev_ok)
 
 
-def check_outfile(f, blkdev_ok=False):
+def check_outfile(f, *, blkdev_ok=False):
 	return _check_file_type_and_access(f, 'output file', blkdev_ok=blkdev_ok)
 	return _check_file_type_and_access(f, 'output file', blkdev_ok=blkdev_ok)
 
 
+def check_outfile_dir(fn, *, blkdev_ok=False):
+	return _check_file_type_and_access(
+		os.path.dirname(os.path.abspath(fn)), 'output directory', blkdev_ok=blkdev_ok)
+
 def check_outdir(f):
 def check_outdir(f):
 	return _check_file_type_and_access(f, 'output directory')
 	return _check_file_type_and_access(f, 'output directory')
 
 
-def get_seed_file(cfg, nargs, wallets=None, invoked_as=None):
+def get_seed_file(cfg, *, nargs, wallets=None, invoked_as=None):
 
 
 	wallets = wallets or cfg._args
 	wallets = wallets or cfg._args
 
 
@@ -137,7 +141,7 @@ def get_seed_file(cfg, nargs, wallets=None, invoked_as=None):
 
 
 	return str(wallets[0]) if wallets else (wf, None)[wd_from_opt] # could be a Path instance
 	return str(wallets[0]) if wallets else (wf, None)[wd_from_opt] # could be a Path instance
 
 
-def _open_or_die(filename, mode, silent=False):
+def _open_or_die(filename, mode, *, silent=False):
 	try:
 	try:
 		return open(filename, mode)
 		return open(filename, mode)
 	except:
 	except:
@@ -152,6 +156,7 @@ def write_data_to_file(
 		cfg,
 		cfg,
 		outfile,
 		outfile,
 		data,
 		data,
+		*,
 		desc                  = 'data',
 		desc                  = 'data',
 		ask_write             = False,
 		ask_write             = False,
 		ask_write_prompt      = '',
 		ask_write_prompt      = '',
@@ -279,7 +284,7 @@ def write_data_to_file(
 	else:
 	else:
 		do_file(outfile, ask_write_prompt)
 		do_file(outfile, ask_write_prompt)
 
 
-def get_words_from_file(cfg, infile, desc, quiet=False):
+def get_words_from_file(cfg, infile, *, desc, quiet=False):
 
 
 	if not quiet:
 	if not quiet:
 		cfg._util.qmsg(f'Getting {desc} from file ‘{infile}’')
 		cfg._util.qmsg(f'Getting {desc} from file ‘{infile}’')
@@ -299,6 +304,7 @@ def get_words_from_file(cfg, infile, desc, quiet=False):
 def get_data_from_file(
 def get_data_from_file(
 		cfg,
 		cfg,
 		infile,
 		infile,
+		*,
 		desc   = 'data',
 		desc   = 'data',
 		dash   = False,
 		dash   = False,
 		silent = False,
 		silent = False,
@@ -326,6 +332,7 @@ def get_data_from_file(
 def get_lines_from_file(
 def get_lines_from_file(
 		cfg,
 		cfg,
 		fn,
 		fn,
+		*,
 		desc          = 'data',
 		desc          = 'data',
 		trim_comments = False,
 		trim_comments = False,
 		quiet         = False,
 		quiet         = False,
@@ -338,7 +345,7 @@ def get_lines_from_file(
 		if have_enc_ext or not is_utf8(data):
 		if have_enc_ext or not is_utf8(data):
 			m = ('Attempting to decrypt', 'Decrypting')[have_enc_ext]
 			m = ('Attempting to decrypt', 'Decrypting')[have_enc_ext]
 			cfg._util.qmsg(f'{m} {desc} ‘{fn}’')
 			cfg._util.qmsg(f'{m} {desc} ‘{fn}’')
-			data = Crypto(cfg).mmgen_decrypt_retry(data, desc)
+			data = Crypto(cfg).mmgen_decrypt_retry(data, desc=desc)
 		return data
 		return data
 
 
 	lines = decrypt_file_maybe().decode().splitlines()
 	lines = decrypt_file_maybe().decode().splitlines()

+ 9 - 0
mmgen/help/__init__.py

@@ -33,6 +33,15 @@ def version(cfg):
 	""", indent='  ').rstrip())
 	""", indent='  ').rstrip())
 	sys.exit(0)
 	sys.exit(0)
 
 
+def list_daemon_ids(cfg):
+	from ..daemon import CoinDaemon
+	from ..util import msg, fmt_list
+	msg('  {} {}'.format('Coin', 'Daemon IDs'))
+	msg('  {} {}'.format('----', '----------'))
+	for k, v in CoinDaemon.coins.items():
+		msg('  {}  {}'.format(k, fmt_list(v.daemon_ids, fmt='barest')))
+	sys.exit(0)
+
 def show_hash_presets(cfg):
 def show_hash_presets(cfg):
 	fs = '      {:<6} {:<3} {:<2} {}'
 	fs = '      {:<6} {:<3} {:<2} {}'
 	from ..util import msg
 	from ..util import msg

+ 5 - 1
mmgen/help/help_notes.py

@@ -30,7 +30,7 @@ class help_notes:
 	def account_info_desc(self):
 	def account_info_desc(self):
 		return 'unspent outputs' if self.proto.base_proto == 'Bitcoin' else 'account info'
 		return 'unspent outputs' if self.proto.base_proto == 'Bitcoin' else 'account info'
 
 
-	def fee_spec_letters(self, use_quotes=False):
+	def fee_spec_letters(self, *, use_quotes=False):
 		cu = self.proto.coin_amt.units
 		cu = self.proto.coin_amt.units
 		sep, conj = ((',', ' or '), ("','", "' or '"))[use_quotes]
 		sep, conj = ((',', ' or '), ("','", "' or '"))[use_quotes]
 		return sep.join(u[0] for u in cu[:-1]) + ('', conj)[len(cu)>1] + cu[-1][0]
 		return sep.join(u[0] for u in cu[:-1]) + ('', conj)[len(cu)>1] + cu[-1][0]
@@ -106,6 +106,10 @@ FMT CODES:
 		from ..util import fmt_list
 		from ..util import fmt_list
 		return fmt_list(CoinDaemon.get_network_ids(self.cfg), fmt='bare')
 		return fmt_list(CoinDaemon.get_network_ids(self.cfg), fmt='bare')
 
 
+	def tx_proxies(self):
+		from ..util import fmt_list
+		return fmt_list(self.cfg._autoset_opts['tx_proxy'].choices, fmt='fancy')
+
 	def rel_fee_desc(self):
 	def rel_fee_desc(self):
 		from ..tx import BaseTX
 		from ..tx import BaseTX
 		return BaseTX(cfg=self.cfg, proto=self.proto).rel_fee_desc
 		return BaseTX(cfg=self.cfg, proto=self.proto).rel_fee_desc

+ 1 - 1
mmgen/help/txsign.py

@@ -19,7 +19,7 @@ from ..daemon import CoinDaemon
 def help(proto, cfg):
 def help(proto, cfg):
 
 
 	def coind_exec():
 	def coind_exec():
-		return CoinDaemon(cfg, proto.coin).exec_fn if proto.coin in CoinDaemon.coins else 'bitcoind'
+		return CoinDaemon(cfg, network_id=proto.coin).exec_fn if proto.coin in CoinDaemon.coins else 'bitcoind'
 
 
 	return """
 	return """
 Transactions may contain both {pnm} or non-{pnm} input addresses.
 Transactions may contain both {pnm} or non-{pnm} input addresses.

+ 2 - 2
mmgen/key.py

@@ -69,7 +69,7 @@ class PrivKey(bytes, InitErrors, MMGenObject):
 	wif        = ImmutableAttr(WifKey, typeconv=False)
 	wif        = ImmutableAttr(WifKey, typeconv=False)
 
 
 	# initialize with (priv_bin, compressed), WIF or self
 	# initialize with (priv_bin, compressed), WIF or self
-	def __new__(cls, proto, s=None, compressed=None, wif=None, pubkey_type=None):
+	def __new__(cls, proto, s=None, *, compressed=None, wif=None, pubkey_type=None):
 		if isinstance(s, cls):
 		if isinstance(s, cls):
 			return s
 			return s
 		if wif:
 		if wif:
@@ -103,7 +103,7 @@ class PrivKey(bytes, InitErrors, MMGenObject):
 					assert type(compressed) is bool, (
 					assert type(compressed) is bool, (
 						f"'compressed' must be of type bool, not {type(compressed).__name__}")
 						f"'compressed' must be of type bool, not {type(compressed).__name__}")
 					me = bytes.__new__(cls, proto.preprocess_key(s, pubkey_type))
 					me = bytes.__new__(cls, proto.preprocess_key(s, pubkey_type))
-					me.wif = WifKey(proto, proto.encode_wif(me, pubkey_type, compressed))
+					me.wif = WifKey(proto, proto.encode_wif(me, pubkey_type, compressed=compressed))
 					me.compressed = compressed
 					me.compressed = compressed
 				me.pubkey_type = pubkey_type
 				me.pubkey_type = pubkey_type
 				me.orig_bytes = s # save the non-preprocessed key
 				me.orig_bytes = s # save the non-preprocessed key

+ 3 - 3
mmgen/keygen.py

@@ -51,7 +51,7 @@ class keygen_base:
 		return None
 		return None
 
 
 	@classmethod
 	@classmethod
-	def get_clsname(cls, cfg, silent=False):
+	def get_clsname(cls, cfg, *, silent=False):
 		return cls.__name__
 		return cls.__name__
 
 
 backend_data = {
 backend_data = {
@@ -78,7 +78,7 @@ def get_pubkey_type_cls(pubkey_type):
 		importlib.import_module(f'mmgen.proto.{backend_data[pubkey_type]["package"]}.keygen'),
 		importlib.import_module(f'mmgen.proto.{backend_data[pubkey_type]["package"]}.keygen'),
 		'backend')
 		'backend')
 
 
-def _check_backend(cfg, backend, pubkey_type, desc='keygen backend'):
+def _check_backend(cfg, backend, pubkey_type, *, desc='keygen backend'):
 
 
 	from .util import is_int, die
 	from .util import is_int, die
 
 
@@ -104,7 +104,7 @@ def check_backend(cfg, proto, backend, addr_type):
 
 
 	return  _check_backend(cfg, backend, pubkey_type, desc='--keygen-backend parameter')
 	return  _check_backend(cfg, backend, pubkey_type, desc='--keygen-backend parameter')
 
 
-def KeyGenerator(cfg, proto, pubkey_type, backend=None, silent=False):
+def KeyGenerator(cfg, proto, pubkey_type, *, backend=None, silent=False):
 	"""
 	"""
 	factory function returning a key generator backend for the specified pubkey type
 	factory function returning a key generator backend for the specified pubkey type
 	"""
 	"""

+ 10 - 3
mmgen/led.py

@@ -33,7 +33,14 @@ class LEDControl:
 	class binfo(Lockable):
 	class binfo(Lockable):
 		_reset_ok = ('trigger_reset',)
 		_reset_ok = ('trigger_reset',)
 
 
-		def __init__(self, name, control, trigger=None, trigger_dfl='heartbeat', trigger_disable='none'):
+		def __init__(
+				self,
+				*,
+				name,
+				control,
+				trigger         = None,
+				trigger_dfl     = 'heartbeat',
+				trigger_disable = 'none'):
 			self.name = name
 			self.name = name
 			self.control = control
 			self.control = control
 			self.trigger = trigger
 			self.trigger = trigger
@@ -71,7 +78,7 @@ class LEDControl:
 			trigger = '/tmp/led_trigger'),
 			trigger = '/tmp/led_trigger'),
 	}
 	}
 
 
-	def __init__(self, enabled, simulate=False, debug=False):
+	def __init__(self, *, enabled, simulate=False, debug=False):
 
 
 		self.enabled = enabled
 		self.enabled = enabled
 		self.debug = debug or simulate
 		self.debug = debug or simulate
@@ -119,7 +126,7 @@ class LEDControl:
 				))
 				))
 				sys.exit(1)
 				sys.exit(1)
 
 
-		def init_state(fn, desc, init_val=None):
+		def init_state(fn, *, desc, init_val=None):
 			try:
 			try:
 				write_init_val(fn, init_val)
 				write_init_val(fn, init_val)
 			except PermissionError:
 			except PermissionError:

+ 2 - 2
mmgen/main_addrgen.py

@@ -141,12 +141,12 @@ if cfg.keygen_backend:
 idxs = addrlist.AddrIdxList(fmt_str=cfg._args.pop())
 idxs = addrlist.AddrIdxList(fmt_str=cfg._args.pop())
 
 
 from .fileutil import get_seed_file
 from .fileutil import get_seed_file
-sf = get_seed_file(cfg, 1)
+sf = get_seed_file(cfg, nargs=1)
 
 
 from .ui import do_license_msg
 from .ui import do_license_msg
 do_license_msg(cfg)
 do_license_msg(cfg)
 
 
-ss = Wallet(cfg, sf)
+ss = Wallet(cfg, fn=sf)
 
 
 ss_seed = ss.seed if cfg.subwallet is None else ss.seed.subseed(cfg.subwallet, print_msg=True)
 ss_seed = ss.seed if cfg.subwallet is None else ss.seed.subseed(cfg.subwallet, print_msg=True)
 
 

+ 2 - 2
mmgen/main_addrimport.py

@@ -84,7 +84,7 @@ addrimport_msgs = {
 def parse_cmd_args(rpc, cmd_args):
 def parse_cmd_args(rpc, cmd_args):
 
 
 	def import_mmgen_list(infile):
 	def import_mmgen_list(infile):
-		return (AddrList, KeyAddrList)[bool(cfg.keyaddr_file)](cfg, proto, infile)
+		return (AddrList, KeyAddrList)[bool(cfg.keyaddr_file)](cfg, proto, infile=infile)
 
 
 	if len(cmd_args) == 1:
 	if len(cmd_args) == 1:
 		infile = cmd_args[0]
 		infile = cmd_args[0]
@@ -97,7 +97,7 @@ def parse_cmd_args(rpc, cmd_args):
 				addrlist = get_lines_from_file(
 				addrlist = get_lines_from_file(
 					cfg,
 					cfg,
 					infile,
 					infile,
-					f'non-{gc.proj_name} addresses',
+					desc = f'non-{gc.proj_name} addresses',
 					trim_comments = True))
 					trim_comments = True))
 		else:
 		else:
 			al = import_mmgen_list(infile)
 			al = import_mmgen_list(infile)

+ 1 - 1
mmgen/main_autosign.py

@@ -211,7 +211,7 @@ if cmd not in ('sign', 'wait'):
 		if getattr(cfg, opt):
 		if getattr(cfg, opt):
 			die(1, f'--{opt.replace("_", "-")} makes no sense for the ‘{cmd}’ operation')
 			die(1, f'--{opt.replace("_", "-")} makes no sense for the ‘{cmd}’ operation')
 
 
-asi = Autosign(cfg, cmd)
+asi = Autosign(cfg, cmd=cmd)
 
 
 cfg._post_init()
 cfg._post_init()
 
 

+ 5 - 5
mmgen/main_msg.py

@@ -61,13 +61,13 @@ class MsgOps:
 
 
 	class verify(sign):
 	class verify(sign):
 
 
-		async def __init__(self, msgfile, addr=None):
+		async def __init__(self, msgfile, *, addr=None):
 			try:
 			try:
 				m = SignedOnlineMsg(cfg, infile=msgfile)
 				m = SignedOnlineMsg(cfg, infile=msgfile)
 			except:
 			except:
 				m = ExportedMsgSigs(cfg, infile=msgfile)
 				m = ExportedMsgSigs(cfg, infile=msgfile)
 
 
-			nSigs = await m.verify(addr)
+			nSigs = await m.verify(addr=addr)
 
 
 			summary = f'{nSigs} signature{suf(nSigs)} verified'
 			summary = f'{nSigs} signature{suf(nSigs)} verified'
 
 
@@ -81,13 +81,13 @@ class MsgOps:
 
 
 	class export(sign):
 	class export(sign):
 
 
-		async def __init__(self, msgfile, addr=None):
+		async def __init__(self, msgfile, *, addr=None):
 
 
 			from .fileutil import write_data_to_file
 			from .fileutil import write_data_to_file
 			write_data_to_file(
 			write_data_to_file(
 				cfg     = cfg,
 				cfg     = cfg,
 				outfile = 'signatures.json',
 				outfile = 'signatures.json',
-				data    = SignedOnlineMsg(cfg, infile=msgfile).get_json_for_export(addr),
+				data    = SignedOnlineMsg(cfg, infile=msgfile).get_json_for_export(addr=addr),
 				desc    = 'signature data')
 				desc    = 'signature data')
 
 
 opts_data = {
 opts_data = {
@@ -223,7 +223,7 @@ async def main():
 	elif op in ('verify', 'export'):
 	elif op in ('verify', 'export'):
 		if len(cmd_args) not in (1, 2):
 		if len(cmd_args) not in (1, 2):
 			cfg._usage()
 			cfg._usage()
-		await getattr(MsgOps, op)(cmd_args[0], cmd_args[1] if len(cmd_args) == 2 else None)
+		await getattr(MsgOps, op)(cmd_args[0], addr=cmd_args[1] if len(cmd_args) == 2 else None)
 	else:
 	else:
 		die(1, f'{op!r}: unrecognized operation')
 		die(1, f'{op!r}: unrecognized operation')
 
 

+ 2 - 2
mmgen/main_passgen.py

@@ -146,7 +146,7 @@ pw_idxs = AddrIdxList(fmt_str=cfg._args.pop())
 pw_id_str = cfg._args.pop()
 pw_id_str = cfg._args.pop()
 
 
 from .fileutil import get_seed_file
 from .fileutil import get_seed_file
-sf = get_seed_file(cfg, 1)
+sf = get_seed_file(cfg, nargs=1)
 
 
 pw_fmt = cfg.passwd_fmt or PasswordList.dfl_pw_fmt
 pw_fmt = cfg.passwd_fmt or PasswordList.dfl_pw_fmt
 pw_len = pwi[pw_fmt].dfl_len // 2 if cfg.passwd_len in ('h', 'H') else cfg.passwd_len
 pw_len = pwi[pw_fmt].dfl_len // 2 if cfg.passwd_len in ('h', 'H') else cfg.passwd_len
@@ -165,7 +165,7 @@ PasswordList(
 from .ui import do_license_msg
 from .ui import do_license_msg
 do_license_msg(cfg)
 do_license_msg(cfg)
 
 
-ss = Wallet(cfg, sf)
+ss = Wallet(cfg, fn=sf)
 
 
 al = PasswordList(
 al = PasswordList(
 	cfg       = cfg,
 	cfg       = cfg,

+ 1 - 1
mmgen/main_seedjoin.py

@@ -131,7 +131,7 @@ do_license_msg(cfg)
 cfg._util.qmsg('Input files:\n  {}\n'.format('\n  '.join(cfg._args)))
 cfg._util.qmsg('Input files:\n  {}\n'.format('\n  '.join(cfg._args)))
 
 
 shares = [Wallet(cfg).seed] if cfg.hidden_incog_input_params else []
 shares = [Wallet(cfg).seed] if cfg.hidden_incog_input_params else []
-shares += [Wallet(cfg,fn).seed for fn in cfg._args]
+shares += [Wallet(cfg, fn=fn).seed for fn in cfg._args]
 
 
 if cfg.master_share:
 if cfg.master_share:
 	share1 = SeedShareMasterJoining(cfg, master_idx, shares[0], id_str, len(shares)).derived_seed
 	share1 = SeedShareMasterJoining(cfg, master_idx, shares[0], id_str, len(shares)).derived_seed

+ 7 - 4
mmgen/main_tool.py

@@ -179,7 +179,7 @@ mods = {
 def get_cmds():
 def get_cmds():
 	return [cmd for mod, cmds in mods.items() if mod != 'help' for cmd in cmds]
 	return [cmd for mod, cmds in mods.items() if mod != 'help' for cmd in cmds]
 
 
-def create_call_sig(cmd, cls, as_string=False):
+def create_call_sig(cmd, cls, *, as_string=False):
 
 
 	m = getattr(cls, cmd)
 	m = getattr(cls, cmd)
 
 
@@ -189,8 +189,11 @@ def create_call_sig(cmd, cls, as_string=False):
 		args, dfls, ann = va['args'], va['dfls'], va['annots']
 		args, dfls, ann = va['args'], va['dfls'], va['annots']
 	else:
 	else:
 		flag = None
 		flag = None
-		args = m.__code__.co_varnames[1:m.__code__.co_argcount]
-		dfls = m.__defaults__ or ()
+		c = m.__code__
+		args = c.co_varnames[1:c.co_argcount + c.co_posonlyargcount + c.co_kwonlyargcount]
+		dfls = (
+			(m.__defaults__ or ()) +
+			tuple(m.__kwdefaults__[k] for k in args if k in (m.__kwdefaults__ or ())))
 		ann  = m.__annotations__
 		ann  = m.__annotations__
 
 
 	nargs = len(args) - len(dfls)
 	nargs = len(args) - len(dfls)
@@ -295,7 +298,7 @@ def process_args(cmd, cmd_args, cls):
 
 
 	return (args, kwargs)
 	return (args, kwargs)
 
 
-def process_result(ret, pager=False, print_result=False):
+def process_result(ret, *, pager=False, print_result=False):
 	"""
 	"""
 	Convert result to something suitable for output to screen and return it.
 	Convert result to something suitable for output to screen and return it.
 	If result is bytes and not convertible to utf8, output as binary using os.write().
 	If result is bytes and not convertible to utf8, output as binary using os.write().

+ 84 - 26
mmgen/main_txsend.py

@@ -34,22 +34,36 @@ opts_data = {
 		'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
---, --longhelp  Print help message for long (global) 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
--q, --quiet     Suppress warnings; overwrite files without prompting
--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
--y, --yes       Answer 'yes' to prompts, suppress non-essential output
+-h, --help       Print this help message
+--, --longhelp   Print help message for long (global) 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
+-H, --dump-hex=F Instead of sending to the network, dump the transaction hex
+                 to file ‘F’.  Use filename ‘-’ to dump to standard output.
+-m, --mark-sent  Mark the transaction as sent by adding it to the removable
+                 device.  Used in combination with --autosign when a trans-
+                 action has been successfully sent out-of-band.
+-n, --tx-proxy=P Send transaction via public TX proxy ‘P’ (supported proxies:
+                 {tx_proxies}).  This is done via a publicly accessible web
+                 page, so no API key or registration is required
+-q, --quiet      Suppress warnings; overwrite files without prompting
+-s, --status     Get status of a sent transaction (or current transaction,
+                 whether sent or unsent, when used with --autosign)
+-t, --test       Test whether the transaction can be sent without sending it
+-v, --verbose    Be more verbose
+-x, --proxy=P    Connect to TX proxy via SOCKS5 proxy ‘P’ (host:port)
+-y, --yes        Answer 'yes' to prompts, suppress non-essential output
 """
 """
+	},
+	'code': {
+		'options': lambda cfg, proto, help_notes, s: s.format(
+			tx_proxies = help_notes('tx_proxies'))
 	}
 	}
 }
 }
 
 
@@ -58,6 +72,20 @@ cfg = Config(opts_data=opts_data)
 if cfg.autosign and cfg.outdir:
 if cfg.autosign and cfg.outdir:
 	die(1, '--outdir cannot be used in combination with --autosign')
 	die(1, '--outdir cannot be used in combination with --autosign')
 
 
+if cfg.mark_sent and not cfg.autosign:
+	die(1, '--mark-sent is used only in combination with --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)
+
 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
@@ -84,9 +112,18 @@ if not cfg.status:
 	from .ui import do_license_msg
 	from .ui import do_license_msg
 	do_license_msg(cfg)
 	do_license_msg(cfg)
 
 
-async def main():
+from .tx import OnlineSignedTX, SentTX
+from .ui import keypress_confirm
+
+async def post_send(tx):
+	tx2 = await SentTX(cfg=cfg, data=tx.__dict__, automount=cfg.autosign)
+	tx2.file.write(
+		outdir        = asi.txauto_dir if cfg.autosign else None,
+		ask_overwrite = False,
+		ask_write     = False)
+	tx2.post_write()
 
 
-	from .tx import OnlineSignedTX, SentTX
+async def main():
 
 
 	if cfg.status and cfg.autosign:
 	if cfg.status and cfg.autosign:
 		tx = await si.get_last_created()
 		tx = await si.get_last_created()
@@ -102,6 +139,10 @@ async def main():
 
 
 	cfg._util.vmsg(f'Getting {tx.desc} ‘{tx.infile}’')
 	cfg._util.vmsg(f'Getting {tx.desc} ‘{tx.infile}’')
 
 
+	if cfg.mark_sent:
+		await post_send(tx)
+		sys.exit(0)
+
 	if cfg.status:
 	if cfg.status:
 		if tx.coin_txid:
 		if tx.coin_txid:
 			cfg._util.qmsg(f'{tx.proto.coin} txid: {tx.coin_txid.hl()}')
 			cfg._util.qmsg(f'{tx.proto.coin} txid: {tx.coin_txid.hl()}')
@@ -110,8 +151,8 @@ async def main():
 			tx.info.view_with_prompt('View transaction details?', pause=False)
 			tx.info.view_with_prompt('View transaction details?', pause=False)
 		sys.exit(retval)
 		sys.exit(retval)
 
 
-	if tx.is_swap:
-		tx.check_swap_expiry()
+	if tx.is_swap and not tx.check_swap_expiry():
+		die(1, 'Swap quote has expired. Please re-create the transaction')
 
 
 	if not cfg.yes:
 	if not cfg.yes:
 		tx.info.view_with_prompt('View transaction details?')
 		tx.info.view_with_prompt('View transaction details?')
@@ -119,12 +160,29 @@ async def main():
 			if not cfg.autosign:
 			if not cfg.autosign:
 				tx.file.write(ask_write_default_yes=True)
 				tx.file.write(ask_write_default_yes=True)
 
 
-	if await tx.send():
-		tx2 = await SentTX(cfg=cfg, data=tx.__dict__, automount=cfg.autosign)
-		tx2.file.write(
-			outdir        = asi.txauto_dir if cfg.autosign else None,
-			ask_overwrite = False,
-			ask_write     = False)
-		tx2.post_write()
+	if cfg.dump_hex:
+		from .fileutil import write_data_to_file
+		write_data_to_file(
+				cfg,
+				cfg.dump_hex,
+				tx.serialized + '\n',
+				desc = 'serialized transaction hex data',
+				ask_overwrite = False,
+				ask_tty = False)
+		if cfg.autosign:
+			if keypress_confirm(cfg, 'Mark transaction as sent on removable device?'):
+				await post_send(tx)
+		else:
+			await post_send(tx)
+	elif cfg.tx_proxy:
+		from .tx.tx_proxy import send_tx
+		if send_tx(cfg, tx):
+			if (not cfg.autosign or
+				keypress_confirm(cfg, 'Mark transaction as sent on removable device?')):
+				await post_send(tx)
+	elif cfg.test:
+		await tx.test_sendable()
+	elif await tx.send():
+		await post_send(tx)
 
 
 async_run(main())
 async_run(main())

+ 1 - 1
mmgen/main_txsign.py

@@ -151,7 +151,7 @@ async def main():
 		kal = get_keyaddrlist(cfg, tx1.proto)
 		kal = get_keyaddrlist(cfg, tx1.proto)
 		kl = get_keylist(cfg)
 		kl = get_keylist(cfg)
 
 
-		tx2 = await txsign(cfg, tx1, seed_files, kl, kal, tx_num_disp)
+		tx2 = await txsign(cfg, tx1, seed_files, kl, kal, tx_num_str=tx_num_disp)
 		if tx2:
 		if tx2:
 			if not cfg.yes:
 			if not cfg.yes:
 				tx2.add_comment() # edits an existing comment
 				tx2.add_comment() # edits an existing comment

+ 2 - 2
mmgen/main_wallet.py

@@ -179,7 +179,7 @@ if cmd_args:
 		cfg._usage()
 		cfg._usage()
 	check_infile(cmd_args[0])
 	check_infile(cmd_args[0])
 
 
-sf = get_seed_file(cfg, nargs, invoked_as=invoked_as)
+sf = get_seed_file(cfg, nargs=nargs, invoked_as=invoked_as)
 
 
 if invoked_as != 'chk':
 if invoked_as != 'chk':
 	from .ui import do_license_msg
 	from .ui import do_license_msg
@@ -212,7 +212,7 @@ if invoked_as == 'subgen':
 		cfg      = cfg,
 		cfg      = cfg,
 		seed_bin = ss_in.seed.subseed(ss_idx, print_msg=True).data)
 		seed_bin = ss_in.seed.subseed(ss_idx, print_msg=True).data)
 elif invoked_as == 'seedsplit':
 elif invoked_as == 'seedsplit':
-	shares = ss_in.seed.split(sss.count, sss.id, master_share)
+	shares = ss_in.seed.split(sss.count, id_str=sss.id, master_idx=master_share)
 	seed_out = shares.get_share_by_idx(sss.idx, base_seed=True)
 	seed_out = shares.get_share_by_idx(sss.idx, base_seed=True)
 	msg(seed_out.get_desc(ui=True))
 	msg(seed_out.get_desc(ui=True))
 	ss_out = Wallet(
 	ss_out = Wallet(

+ 1 - 1
mmgen/main_xmrwallet.py

@@ -143,7 +143,7 @@ elif op in ('export-outputs', 'export-outputs-sign', 'import-key-images'):
 else:
 else:
 	die(1, f'{op!r}: unrecognized operation')
 	die(1, f'{op!r}: unrecognized operation')
 
 
-m = xmrwallet.op(op, cfg, infile, wallets, spec)
+m = xmrwallet.op(op, cfg, infile, wallets, spec=spec)
 
 
 if asyncio.run(m.main()):
 if asyncio.run(m.main()):
 	m.post_main_success()
 	m.post_main_success()

+ 4 - 4
mmgen/mn_entry.py

@@ -261,7 +261,7 @@ class MnemonicEntry:
 			self._usl = usl
 			self._usl = usl
 		return self._usl
 		return self._usl
 
 
-	def idx(self, w, entry_mode, lo_idx=None, hi_idx=None):
+	def idx(self, w, entry_mode, *, lo_idx=None, hi_idx=None):
 		"""
 		"""
 		Return values:
 		Return values:
 		  - all modes:
 		  - all modes:
@@ -306,7 +306,7 @@ class MnemonicEntry:
 			msg('  {}) {:8} {}'.format(
 			msg('  {}) {:8} {}'.format(
 				n,
 				n,
 				mode.name + ':',
 				mode.name + ':',
-				fmt(mode.choose_info, ' '*14).lstrip().format(usl=self.uniq_ss_len),
+				fmt(mode.choose_info, indent=' '*14).lstrip().format(usl=self.uniq_ss_len),
 			))
 			))
 		prompt = f'Type a number, or hit ENTER for the default ({capfirst(self.dfl_entry_mode)}): '
 		prompt = f'Type a number, or hit ENTER for the default ({capfirst(self.dfl_entry_mode)}): '
 		erase = '\r' + ' ' * (len(prompt)+19) + '\r'
 		erase = '\r' + ' ' * (len(prompt)+19) + '\r'
@@ -323,7 +323,7 @@ class MnemonicEntry:
 				time.sleep(self.cfg.err_disp_timeout)
 				time.sleep(self.cfg.err_disp_timeout)
 				msg_r(erase)
 				msg_r(erase)
 
 
-	def get_mnemonic_from_user(self, mn_len, validate=True):
+	def get_mnemonic_from_user(self, mn_len, *, validate=True):
 		mll = list(self.bconv.seedlen_map_rev)
 		mll = list(self.bconv.seedlen_map_rev)
 		assert mn_len in mll, f'{mn_len}: invalid mnemonic length (must be one of {mll})'
 		assert mn_len in mll, f'{mn_len}: invalid mnemonic length (must be one of {mll})'
 
 
@@ -418,7 +418,7 @@ class MnemonicEntryMonero(MnemonicEntry):
 	dfl_entry_mode = 'short'
 	dfl_entry_mode = 'short'
 	has_chksum = True
 	has_chksum = True
 
 
-def mn_entry(cfg, wl_id, entry_mode=None):
+def mn_entry(cfg, wl_id, *, entry_mode=None):
 	if wl_id == 'words':
 	if wl_id == 'words':
 		wl_id = 'mmgen'
 		wl_id = 'mmgen'
 	me = MnemonicEntry.get_cls_by_wordlist(wl_id)(cfg)
 	me = MnemonicEntry.get_cls_by_wordlist(wl_id)(cfg)

+ 5 - 5
mmgen/msg.py

@@ -38,7 +38,7 @@ class MMGenIDRange(HiliteStr, InitErrors, MMGenObject):
 			t = proto.addr_type((ss[1], proto.dfl_mmtype)[len(ss)==2])
 			t = proto.addr_type((ss[1], proto.dfl_mmtype)[len(ss)==2])
 			me = str.__new__(cls, '{}:{}:{}'.format(ss[0], t, ss[-1]))
 			me = str.__new__(cls, '{}:{}:{}'.format(ss[0], t, ss[-1]))
 			me.sid = SeedID(sid=ss[0])
 			me.sid = SeedID(sid=ss[0])
-			me.idxlist = AddrIdxList(ss[-1])
+			me.idxlist = AddrIdxList(fmt_str=ss[-1])
 			me.mmtype = t
 			me.mmtype = t
 			assert t in proto.mmtypes, f'{t}: invalid address type for {proto.cls_name}'
 			assert t in proto.mmtypes, f'{t}: invalid address type for {proto.cls_name}'
 			me.al_id = str.__new__(AddrListID, me.sid+':'+me.mmtype) # checks already done
 			me.al_id = str.__new__(AddrListID, me.sid+':'+me.mmtype) # checks already done
@@ -91,7 +91,7 @@ class coin_msg:
 			coin, network = network_id.split('_')
 			coin, network = network_id.split('_')
 			return init_proto(cfg=cfg, coin=coin, network=network)
 			return init_proto(cfg=cfg, coin=coin, network=network)
 
 
-		def write_to_file(self, outdir=None, ask_overwrite=False):
+		def write_to_file(self, *, outdir=None, ask_overwrite=False):
 			data = {
 			data = {
 				'id': f'{gc.proj_name} {self.desc}',
 				'id': f'{gc.proj_name} {self.desc}',
 				'metadata': self.data,
 				'metadata': self.data,
@@ -208,7 +208,7 @@ class coin_msg:
 
 
 	class unsigned(completed):
 	class unsigned(completed):
 
 
-		async def sign(self, wallet_files, passwd_file=None):
+		async def sign(self, wallet_files, *, passwd_file=None):
 
 
 			from .addrlist import KeyAddrList
 			from .addrlist import KeyAddrList
 
 
@@ -300,7 +300,7 @@ class coin_msg:
 
 
 			return sigs
 			return sigs
 
 
-		async def verify(self, addr=None):
+		async def verify(self, *, addr=None):
 
 
 			sigs = self.get_sigs(addr)
 			sigs = self.get_sigs(addr)
 
 
@@ -319,7 +319,7 @@ class coin_msg:
 
 
 			return len(sigs)
 			return len(sigs)
 
 
-		def get_json_for_export(self, addr=None):
+		def get_json_for_export(self, *, addr=None):
 			sigs = list(self.get_sigs(addr).values())
 			sigs = list(self.get_sigs(addr).values())
 			pfx = self.msg_cls.sigdata_pfx
 			pfx = self.msg_cls.sigdata_pfx
 			if pfx:
 			if pfx:

+ 12 - 9
mmgen/obj.py

@@ -102,7 +102,7 @@ class ImmutableAttr: # Descriptor
 	"""
 	"""
 	ok_dtypes = (type, type(None), type(lambda:0))
 	ok_dtypes = (type, type(None), type(lambda:0))
 
 
-	def __init__(self, dtype, typeconv=True, set_none_ok=False, include_proto=False):
+	def __init__(self, dtype, *, typeconv=True, set_none_ok=False, include_proto=False):
 		self.set_none_ok = set_none_ok
 		self.set_none_ok = set_none_ok
 		self.typeconv = typeconv
 		self.typeconv = typeconv
 
 
@@ -154,7 +154,7 @@ class ListItemAttr(ImmutableAttr):
 	For attributes that might not be present in the data instance
 	For attributes that might not be present in the data instance
 	Reassignment or deletion allowed if specified
 	Reassignment or deletion allowed if specified
 	"""
 	"""
-	def __init__(self, dtype, typeconv=True, include_proto=False, reassign_ok=False, delete_ok=False):
+	def __init__(self, dtype, *, typeconv=True, include_proto=False, reassign_ok=False, delete_ok=False):
 		self.reassign_ok = reassign_ok
 		self.reassign_ok = reassign_ok
 		self.delete_ok = delete_ok
 		self.delete_ok = delete_ok
 		ImmutableAttr.__init__(self, dtype, typeconv=typeconv, include_proto=include_proto)
 		ImmutableAttr.__init__(self, dtype, typeconv=typeconv, include_proto=include_proto)
@@ -264,7 +264,7 @@ class Int(int, Hilite, InitErrors):
 	max_digits = None
 	max_digits = None
 	color = 'red'
 	color = 'red'
 
 
-	def __new__(cls, n, base=10):
+	def __new__(cls, n, *, base=10):
 		if isinstance(n, cls):
 		if isinstance(n, cls):
 			return n
 			return n
 		try:
 		try:
@@ -280,11 +280,11 @@ class Int(int, Hilite, InitErrors):
 			return cls.init_fail(e, n)
 			return cls.init_fail(e, n)
 
 
 	@classmethod
 	@classmethod
-	def fmtc(cls, s, width, color=False):
-		return super().fmtc(str(s), width=width, color=color)
+	def fmtc(cls, s, width, /, *, color=False):
+		return super().fmtc(str(s), width, color=color)
 
 
-	def fmt(self, width, color=False):
-		return super().fmtc(str(self), width=width, color=color)
+	def fmt(self, width, /, *, color=False):
+		return super().fmtc(str(self), width, color=color)
 
 
 	def hl(self, **kwargs):
 	def hl(self, **kwargs):
 		return super().colorize(str(self), **kwargs)
 		return super().colorize(str(self), **kwargs)
@@ -306,7 +306,7 @@ class HexStr(HiliteStr, InitErrors):
 	width = None
 	width = None
 	hexcase = 'lower'
 	hexcase = 'lower'
 	trunc_ok = False
 	trunc_ok = False
-	def __new__(cls, s, case=None):
+	def __new__(cls, s, *, case=None):
 		if isinstance(s, cls):
 		if isinstance(s, cls):
 			return s
 			return s
 		if case is None:
 		if case is None:
@@ -324,7 +324,7 @@ class HexStr(HiliteStr, InitErrors):
 		except Exception as e:
 		except Exception as e:
 			return cls.init_fail(e, s)
 			return cls.init_fail(e, s)
 
 
-	def truncate(self, width, color=True):
+	def truncate(self, width, /, *, color=True):
 		return self.colorize(
 		return self.colorize(
 			self if width >= self.width else self[:width-2] + '..',
 			self if width >= self.width else self[:width-2] + '..',
 			color = color)
 			color = color)
@@ -332,6 +332,9 @@ class HexStr(HiliteStr, InitErrors):
 class CoinTxID(HexStr):
 class CoinTxID(HexStr):
 	color, width, hexcase = ('purple', 64, 'lower')
 	color, width, hexcase = ('purple', 64, 'lower')
 
 
+def is_coin_txid(s):
+	return get_obj(CoinTxID, s=s, silent=True, return_bool=True)
+
 class WalletPassword(HexStr):
 class WalletPassword(HexStr):
 	color, width, hexcase = ('blue', 32, 'lower')
 	color, width, hexcase = ('blue', 32, 'lower')
 
 

+ 10 - 8
mmgen/objmethods.py

@@ -49,7 +49,7 @@ class Hilite:
 
 
 	# class method equivalent of fmt()
 	# class method equivalent of fmt()
 	@classmethod
 	@classmethod
-	def fmtc(cls, s, width, color=False):
+	def fmtc(cls, s, width, /, *, color=False):
 		if len(s) > width:
 		if len(s) > width:
 			assert cls.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string"
 			assert cls.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string"
 			return cls.colorize(s[:width].ljust(width), color=color)
 			return cls.colorize(s[:width].ljust(width), color=color)
@@ -57,21 +57,21 @@ class Hilite:
 			return cls.colorize(s.ljust(width), color=color)
 			return cls.colorize(s.ljust(width), color=color)
 
 
 	@classmethod
 	@classmethod
-	def hlc(cls, s, color=True):
+	def hlc(cls, s, *, color=True):
 		return getattr(color_mod, cls.color)(s) if color else s
 		return getattr(color_mod, cls.color)(s) if color else s
 
 
 	@classmethod
 	@classmethod
-	def colorize(cls, s, color=True):
+	def colorize(cls, s, *, color=True):
 		return getattr(color_mod, cls.color)(s) if color else s
 		return getattr(color_mod, cls.color)(s) if color else s
 
 
 	@classmethod
 	@classmethod
-	def colorize2(cls, s, color=True, color_override=''):
+	def colorize2(cls, s, *, color=True, color_override=''):
 		return getattr(color_mod, color_override or cls.color)(s) if color else s
 		return getattr(color_mod, color_override or cls.color)(s) if color else s
 
 
 class HiliteStr(str, Hilite):
 class HiliteStr(str, Hilite):
 
 
 	# supports single-width characters only
 	# supports single-width characters only
-	def fmt(self, width, color=False):
+	def fmt(self, width, /, *, color=False):
 		if len(self) > width:
 		if len(self) > width:
 			assert self.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string"
 			assert self.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string"
 			return self.colorize(self[:width].ljust(width), color=color)
 			return self.colorize(self[:width].ljust(width), color=color)
@@ -82,6 +82,8 @@ class HiliteStr(str, Hilite):
 	def fmt2(
 	def fmt2(
 			self,
 			self,
 			width,                  # screen width - must be at least 2 (one wide char)
 			width,                  # screen width - must be at least 2 (one wide char)
+			/,
+			*,
 			color          = False,
 			color          = False,
 			encl           = '',    # if set, must be exactly 2 single-width chars
 			encl           = '',    # if set, must be exactly 2 single-width chars
 			nullrepl       = '',
 			nullrepl       = '',
@@ -112,12 +114,12 @@ class HiliteStr(str, Hilite):
 		else:
 		else:
 			return self.colorize2(s.ljust(width-s_wide_count), color=color, color_override=color_override)
 			return self.colorize2(s.ljust(width-s_wide_count), color=color, color_override=color_override)
 
 
-	def hl(self, color=True):
+	def hl(self, *, color=True):
 		return getattr(color_mod, self.color)(self) if color else self
 		return getattr(color_mod, self.color)(self) if color else self
 
 
 	# an alternative to hl(), with enclosure and color override
 	# an alternative to hl(), with enclosure and color override
 	# can be called as an unbound method with class as first argument
 	# can be called as an unbound method with class as first argument
-	def hl2(self, s=None, color=True, encl='', color_override=''):
+	def hl2(self, s=None, *, color=True, encl='', color_override=''):
 		if encl:
 		if encl:
 			return self.colorize2(encl[0]+(s or self)+encl[1], color=color, color_override=color_override)
 			return self.colorize2(encl[0]+(s or self)+encl[1], color=color, color_override=color_override)
 		else:
 		else:
@@ -126,7 +128,7 @@ class HiliteStr(str, Hilite):
 class InitErrors:
 class InitErrors:
 
 
 	@classmethod
 	@classmethod
-	def init_fail(cls, e, m, e2=None, m2=None, objname=None, preformat=False):
+	def init_fail(cls, e, m, *, e2=None, m2=None, objname=None, preformat=False):
 
 
 		def get_errmsg():
 		def get_errmsg():
 			ret = m if preformat else (
 			ret = m if preformat else (

+ 7 - 3
mmgen/opts.py

@@ -160,7 +160,7 @@ global_opts_help_pat = re.compile(r'^\t\t\t(.)(.) (?:--([{}a-zA-Z0-9-]{2,64})(=|
 
 
 opt_tuple = namedtuple('cmdline_option', ['name', 'has_parm'])
 opt_tuple = namedtuple('cmdline_option', ['name', 'has_parm'])
 
 
-def parse_opts(cfg, opts_data, global_opts_data, global_filter_codes, need_proto):
+def parse_opts(cfg, opts_data, global_opts_data, global_filter_codes, *, need_proto):
 
 
 	def parse_v1():
 	def parse_v1():
 		for line in opts_data['text']['options'].strip().splitlines():
 		for line in opts_data['text']['options'].strip().splitlines():
@@ -243,6 +243,7 @@ class Opts:
 	def __init__(
 	def __init__(
 			self,
 			self,
 			cfg,
 			cfg,
+			*,
 			opts_data,
 			opts_data,
 			init_opts,    # dict containing opts to pre-initialize
 			init_opts,    # dict containing opts to pre-initialize
 			parsed_opts,
 			parsed_opts,
@@ -261,7 +262,7 @@ class Opts:
 			opts_data,
 			opts_data,
 			self.global_opts_data,
 			self.global_opts_data,
 			self.global_filter_codes,
 			self.global_filter_codes,
-			need_proto)
+			need_proto = need_proto)
 
 
 		cfg._args = po.cmd_args
 		cfg._args = po.cmd_args
 		cfg._uopts = uopts = po.user_opts
 		cfg._uopts = uopts = po.user_opts
@@ -292,7 +293,7 @@ class Opts:
 class UserOpts(Opts):
 class UserOpts(Opts):
 
 
 	help_pkg = 'mmgen.help'
 	help_pkg = 'mmgen.help'
-	info_funcs = ('version', 'show_hash_presets')
+	info_funcs = ('version', 'show_hash_presets', 'list_daemon_ids')
 
 
 	global_opts_data = {
 	global_opts_data = {
 		#  coin code : cmd code : opt : opt param : text
 		#  coin code : cmd code : opt : opt param : text
@@ -312,6 +313,7 @@ class UserOpts(Opts):
 			rr --daemon-data-dir=path Specify coin daemon data directory location
 			rr --daemon-data-dir=path Specify coin daemon data directory location
 			Rr --daemon-id=ID         Specify the coin daemon ID
 			Rr --daemon-id=ID         Specify the coin daemon ID
 			rr --ignore-daemon-version Ignore coin daemon version check
 			rr --ignore-daemon-version Ignore coin daemon version check
+			Rr --list-daemon-ids      List all available daemon IDs
 			rr --http-timeout=t       Set HTTP timeout in seconds for JSON-RPC connections
 			rr --http-timeout=t       Set HTTP timeout in seconds for JSON-RPC connections
 			-- --no-license           Suppress the GPL license prompt
 			-- --no-license           Suppress the GPL license prompt
 			Rr --rpc-host=HOST        Communicate with coin daemon running on host HOST
 			Rr --rpc-host=HOST        Communicate with coin daemon running on host HOST
@@ -322,6 +324,7 @@ class UserOpts(Opts):
 			Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
 			Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
 			-p --regtest=0|1          Disable or enable regtest mode
 			-p --regtest=0|1          Disable or enable regtest mode
 			-- --testnet=0|1          Disable or enable testnet
 			-- --testnet=0|1          Disable or enable testnet
+			-- --test-suite           Use test suite configuration
 			br --tw-name=NAME         Specify alternate name for the BTC/LTC/BCH tracking
 			br --tw-name=NAME         Specify alternate name for the BTC/LTC/BCH tracking
 			+                         wallet (default: ‘{tw_name}’)
 			+                         wallet (default: ‘{tw_name}’)
 			-- --skip-cfg-file        Skip reading the configuration file
 			-- --skip-cfg-file        Skip reading the configuration file
@@ -334,6 +337,7 @@ class UserOpts(Opts):
 			rr   For descriptions, refer to the non-prefixed versions of these options above
 			rr   For descriptions, refer to the non-prefixed versions of these options above
 			rr   Prefixed options override their non-prefixed counterparts
 			rr   Prefixed options override their non-prefixed counterparts
 			rr   OPTION                            SUPPORTED PREFIXES
 			rr   OPTION                            SUPPORTED PREFIXES
+			Rr --PREFIX-daemon-id                btc ltc bch eth etc
 			rr --PREFIX-ignore-daemon-version    btc ltc bch eth etc xmr
 			rr --PREFIX-ignore-daemon-version    btc ltc bch eth etc xmr
 			br --PREFIX-tw-name                  btc ltc bch
 			br --PREFIX-tw-name                  btc ltc bch
 			Rr --PREFIX-rpc-host                 btc ltc bch eth etc
 			Rr --PREFIX-rpc-host                 btc ltc bch eth etc

+ 9 - 12
mmgen/passwdlist.py

@@ -69,6 +69,7 @@ class PasswordList(AddrList):
 			self,
 			self,
 			cfg,
 			cfg,
 			proto,
 			proto,
+			*,
 			infile          = None,
 			infile          = None,
 			seed            = None,
 			seed            = None,
 			pw_idxs         = None,
 			pw_idxs         = None,
@@ -108,7 +109,7 @@ class PasswordList(AddrList):
 		self.chksum = AddrListChksum(self)
 		self.chksum = AddrListChksum(self)
 
 
 		fs = f'{self.al_id.sid}-{self.pw_id_str}-{self.pw_fmt_disp}-{self.pw_len}[{{}}]'
 		fs = f'{self.al_id.sid}-{self.pw_id_str}-{self.pw_fmt_disp}-{self.pw_len}[{{}}]'
-		self.id_str = AddrListIDStr(self, fs)
+		self.id_str = AddrListIDStr(self, fmt_str=fs)
 
 
 		if not skip_chksum_msg:
 		if not skip_chksum_msg:
 			self.do_chksum_msg(record=not infile)
 			self.do_chksum_msg(record=not infile)
@@ -125,22 +126,18 @@ class PasswordList(AddrList):
 			die('InvalidPasswdFormat',
 			die('InvalidPasswdFormat',
 				f'{self.pw_fmt!r}: invalid password format.  Valid formats: {", ".join(self.pw_info)}')
 				f'{self.pw_fmt!r}: invalid password format.  Valid formats: {", ".join(self.pw_info)}')
 
 
-	def chk_pw_len(self, passwd=None):
-		if passwd is None:
-			assert self.pw_len, 'either passwd or pw_len must be set'
-			pw_len = self.pw_len
-			fs = '{l}: invalid user-requested length for {b} ({c}{m})'
-		else:
-			pw_len = len(passwd)
-			fs = '{pw}: {b} has invalid length {l} ({c}{m} characters)'
+	def chk_pw_len(self):
+		assert self.pw_len, 'pw_len must be set'
+		pw_len = self.pw_len
+		fs = '{l}: invalid user-requested length for {b} ({c}{m})'
 		d = self.pw_info[self.pw_fmt]
 		d = self.pw_info[self.pw_fmt]
 		if d.valid_lens:
 		if d.valid_lens:
 			if pw_len not in d.valid_lens:
 			if pw_len not in d.valid_lens:
-				die(2, fs.format(l=pw_len, b=d.desc, c='not one of ', m=d.valid_lens, pw=passwd))
+				die(2, fs.format(l=pw_len, b=d.desc, c='not one of ', m=d.valid_lens))
 		elif pw_len > d.max_len:
 		elif pw_len > d.max_len:
-			die(2, fs.format(l=pw_len, b=d.desc, c='>', m=d.max_len, pw=passwd))
+			die(2, fs.format(l=pw_len, b=d.desc, c='>', m=d.max_len))
 		elif pw_len < d.min_len:
 		elif pw_len < d.min_len:
-			die(2, fs.format(l=pw_len, b=d.desc, c='<', m=d.min_len, pw=passwd))
+			die(2, fs.format(l=pw_len, b=d.desc, c='<', m=d.min_len))
 
 
 	def set_pw_len(self, pw_len):
 	def set_pw_len(self, pw_len):
 		d = self.pw_info[self.pw_fmt]
 		d = self.pw_info[self.pw_fmt]

+ 3 - 3
mmgen/platform/darwin/util.py

@@ -43,7 +43,7 @@ class MacOSRamDisk:
 	desc = 'ramdisk'
 	desc = 'ramdisk'
 	min_size = 10 # 10MB is the minimum supported by hdiutil
 	min_size = 10 # 10MB is the minimum supported by hdiutil
 
 
-	def __init__(self, cfg, label, size, path=None):
+	def __init__(self, cfg, label, size, *, path=None):
 		if size < self.min_size:
 		if size < self.min_size:
 			warn_ramdisk_too_small(size, self.min_size)
 			warn_ramdisk_too_small(size, self.min_size)
 			size = self.min_size
 			size = self.min_size
@@ -59,7 +59,7 @@ class MacOSRamDisk:
 	def get_diskutil_size(self):
 	def get_diskutil_size(self):
 		return get_device_size(self.label) // (2**20)
 		return get_device_size(self.label) // (2**20)
 
 
-	def create(self, quiet=False):
+	def create(self, *, quiet=False):
 		redir = DEVNULL if quiet else None
 		redir = DEVNULL if quiet else None
 		if self.exists():
 		if self.exists():
 			diskutil_size = self.get_diskutil_size()
 			diskutil_size = self.get_diskutil_size()
@@ -81,7 +81,7 @@ class MacOSRamDisk:
 			self.path.mkdir(parents=True, exist_ok=True)
 			self.path.mkdir(parents=True, exist_ok=True)
 			run(['diskutil', 'mount', '-mountPoint', str(self.path.absolute()), self.label], stdout=redir, check=True)
 			run(['diskutil', 'mount', '-mountPoint', str(self.path.absolute()), self.label], stdout=redir, check=True)
 
 
-	def destroy(self, quiet=False):
+	def destroy(self, *, quiet=False):
 		if not self.exists():
 		if not self.exists():
 			self.cfg._util.qmsg(f'{self.desc.capitalize()} {self.label.hl()} at path {self.path} not found')
 			self.cfg._util.qmsg(f'{self.desc.capitalize()} {self.label.hl()} at path {self.path} not found')
 			return
 			return

+ 1 - 1
mmgen/proto/btc/addrdata.py

@@ -25,7 +25,7 @@ class BitcoinTwAddrData(TwAddrData):
 		"""
 		"""
 	}
 	}
 
 
-	async def get_tw_data(self, twctl=None):
+	async def get_tw_data(self, *, twctl=None):
 		self.cfg._util.vmsg('Getting address data from tracking wallet')
 		self.cfg._util.vmsg('Getting address data from tracking wallet')
 		c = self.rpc
 		c = self.rpc
 		if 'label_api' in c.caps:
 		if 'label_api' in c.caps:

+ 2 - 1
mmgen/proto/btc/params.py

@@ -55,6 +55,7 @@ class mainnet(CoinProtocol.Secp256k1): # chainparams.cpp
 	max_op_return_data_len = 80
 	max_op_return_data_len = 80
 
 
 	coin_cfg_opts = (
 	coin_cfg_opts = (
+		'daemon_id',
 		'ignore_daemon_version',
 		'ignore_daemon_version',
 		'rpc_host',
 		'rpc_host',
 		'rpc_port',
 		'rpc_port',
@@ -65,7 +66,7 @@ class mainnet(CoinProtocol.Secp256k1): # chainparams.cpp
 		'cashaddr',
 		'cashaddr',
 	)
 	)
 
 
-	def encode_wif(self, privbytes, pubkey_type, compressed): # input is preprocessed hex
+	def encode_wif(self, privbytes, pubkey_type, *, compressed): # input is preprocessed
 		assert len(privbytes) == self.privkey_len, f'{len(privbytes)} bytes: incorrect private key length!'
 		assert len(privbytes) == self.privkey_len, f'{len(privbytes)} bytes: incorrect private key length!'
 		assert pubkey_type in self.wif_ver_bytes, f'{pubkey_type!r}: invalid pubkey_type'
 		assert pubkey_type in self.wif_ver_bytes, f'{pubkey_type!r}: invalid pubkey_type'
 		return b58chk_encode(
 		return b58chk_encode(

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

@@ -73,7 +73,7 @@ class MMGenRegtest(MMGenObject):
 		'bch': 'n2fxhNx27GhHAWQhyuZ5REcBNrJqCJsJ12',
 		'bch': 'n2fxhNx27GhHAWQhyuZ5REcBNrJqCJsJ12',
 	}
 	}
 
 
-	def __init__(self, cfg, coin, bdb_wallet=False):
+	def __init__(self, cfg, coin, *, bdb_wallet=False):
 		self.cfg = cfg
 		self.cfg = cfg
 		self.coin = coin.lower()
 		self.coin = coin.lower()
 		self.bdb_wallet = bdb_wallet
 		self.bdb_wallet = bdb_wallet
@@ -83,7 +83,7 @@ class MMGenRegtest(MMGenObject):
 		self.proto = init_proto(cfg, self.coin, regtest=True, need_amt=True)
 		self.proto = init_proto(cfg, self.coin, regtest=True, need_amt=True)
 		self.d = CoinDaemon(
 		self.d = CoinDaemon(
 			cfg,
 			cfg,
-			self.coin + '_rt',
+			network_id = self.coin + '_rt',
 			test_suite = cfg.test_suite,
 			test_suite = cfg.test_suite,
 			opts       = ['bdb_wallet'] if bdb_wallet else None)
 			opts       = ['bdb_wallet'] if bdb_wallet else None)
 
 
@@ -116,7 +116,7 @@ class MMGenRegtest(MMGenObject):
 		t.addrtype = 'compressed' if self.proto.coin == 'BCH' else 'bech32'
 		t.addrtype = 'compressed' if self.proto.coin == 'BCH' else 'bech32'
 		return t.hex2wif(self.bdb_hdseed)
 		return t.hex2wif(self.bdb_hdseed)
 
 
-	async def generate(self, blocks=1, silent=False):
+	async def generate(self, blocks=1, *, silent=False):
 
 
 		blocks = int(blocks)
 		blocks = int(blocks)
 
 
@@ -194,11 +194,11 @@ class MMGenRegtest(MMGenObject):
 			msg('Stopping regtest daemon')
 			msg('Stopping regtest daemon')
 			await self.rpc_call('stop')
 			await self.rpc_call('stop')
 
 
-	def init_daemon(self, reindex=False):
+	def init_daemon(self, *, reindex=False):
 		if reindex:
 		if reindex:
 			self.d.usr_coind_args.append('--reindex')
 			self.d.usr_coind_args.append('--reindex')
 
 
-	async def start_daemon(self, reindex=False, silent=True):
+	async def start_daemon(self, *, reindex=False, silent=True):
 		self.init_daemon(reindex=reindex)
 		self.init_daemon(reindex=reindex)
 		self.d.start(silent=silent)
 		self.d.start(silent=silent)
 		for user in ('miner', 'bob', 'alice'):
 		for user in ('miner', 'bob', 'alice'):
@@ -260,7 +260,7 @@ class MMGenRegtest(MMGenObject):
 
 
 	async def fork(self, coin): # currently disabled
 	async def fork(self, coin): # currently disabled
 
 
-		proto = init_proto(self.cfg, coin, False)
+		proto = init_proto(self.cfg, coin, testnet=False)
 		if not [f for f in proto.forks if f[2] == proto.coin.lower() and f[3] is True]:
 		if not [f for f in proto.forks if f[2] == proto.coin.lower() and f[3] is True]:
 			die(1, f'Coin {proto.coin} is not a replayable fork of coin {coin}')
 			die(1, f'Coin {proto.coin} is not a replayable fork of coin {coin}')
 
 

+ 7 - 2
mmgen/proto/btc/rpc.py

@@ -54,6 +54,7 @@ class CallSigs:
 		def createwallet(
 		def createwallet(
 				self,
 				self,
 				wallet_name,
 				wallet_name,
+				*,
 				no_keys         = True,
 				no_keys         = True,
 				blank           = True,
 				blank           = True,
 				passphrase      = '',
 				passphrase      = '',
@@ -86,6 +87,7 @@ class CallSigs:
 		def createwallet(
 		def createwallet(
 				self,
 				self,
 				wallet_name,
 				wallet_name,
+				*,
 				no_keys         = True,
 				no_keys         = True,
 				blank           = True,
 				blank           = True,
 				passphrase      = '',
 				passphrase      = '',
@@ -117,6 +119,7 @@ class BitcoinRPCClient(RPCClient, metaclass=AsyncInit):
 			self,
 			self,
 			cfg,
 			cfg,
 			proto,
 			proto,
+			*,
 			daemon,
 			daemon,
 			backend,
 			backend,
 			ignore_wallet):
 			ignore_wallet):
@@ -268,7 +271,8 @@ class BitcoinRPCClient(RPCClient, metaclass=AsyncInit):
 
 
 		fn = self.get_daemon_cfg_fn()
 		fn = self.get_daemon_cfg_fn()
 		try:
 		try:
-			lines = get_lines_from_file(self.cfg, fn, 'daemon config file', silent=not self.cfg.verbose)
+			lines = get_lines_from_file(
+				self.cfg, fn, desc='daemon config file', silent=not self.cfg.verbose)
 		except:
 		except:
 			self.cfg._util.vmsg(f'Warning: {fn!r} does not exist or is unreadable')
 			self.cfg._util.vmsg(f'Warning: {fn!r} does not exist or is unreadable')
 			return dict((k, None) for k in req_keys)
 			return dict((k, None) for k in req_keys)
@@ -287,7 +291,8 @@ class BitcoinRPCClient(RPCClient, metaclass=AsyncInit):
 
 
 	def get_daemon_auth_cookie(self):
 	def get_daemon_auth_cookie(self):
 		fn = self.daemon.auth_cookie_fn
 		fn = self.daemon.auth_cookie_fn
-		return get_lines_from_file(self.cfg, fn, 'cookie', quiet=True)[0] if os.access(fn, os.R_OK) else ''
+		return get_lines_from_file(
+			self.cfg, fn, desc='cookie', quiet=True)[0] if os.access(fn, os.R_OK) else ''
 
 
 	def info(self, info_id):
 	def info(self, info_id):
 
 

+ 1 - 1
mmgen/proto/btc/tw/addresses.py

@@ -49,7 +49,7 @@ class BitcoinTwAddresses(TwAddresses, BitcoinTwRPC):
 		qmsg = self.cfg._util.qmsg
 		qmsg = self.cfg._util.qmsg
 		qmsg_r = self.cfg._util.qmsg_r
 		qmsg_r = self.cfg._util.qmsg_r
 		qmsg_r('Getting unspent outputs...')
 		qmsg_r('Getting unspent outputs...')
-		addrs = await self.get_unspent_by_mmid(self.minconf)
+		addrs = await self.get_unspent_by_mmid(minconf=self.minconf)
 		qmsg('done')
 		qmsg('done')
 
 
 		coin_amt = self.proto.coin_amt
 		coin_amt = self.proto.coin_amt

+ 2 - 2
mmgen/proto/btc/tw/bal.py

@@ -18,10 +18,10 @@ from ....rpc import rpc_init
 
 
 class BitcoinTwGetBalance(TwGetBalance):
 class BitcoinTwGetBalance(TwGetBalance):
 
 
-	async def __init__(self, cfg, proto, minconf, quiet):
+	async def __init__(self, cfg, proto, *, minconf, quiet):
 		self.rpc = await rpc_init(cfg, proto)
 		self.rpc = await rpc_init(cfg, proto)
 		self.walletinfo = await self.rpc.walletinfo
 		self.walletinfo = await self.rpc.walletinfo
-		await super().__init__(cfg, proto, minconf, quiet)
+		await super().__init__(cfg, proto, minconf=minconf, quiet=quiet)
 
 
 	start_labels = ('TOTAL', 'Non-MMGen', 'Non-wallet')
 	start_labels = ('TOTAL', 'Non-MMGen', 'Non-wallet')
 	conf_cols = {
 	conf_cols = {

+ 1 - 1
mmgen/proto/btc/tw/ctl.py

@@ -27,7 +27,7 @@ class BitcoinTwCtl(TwCtl):
 		raise NotImplementedError('not implemented')
 		raise NotImplementedError('not implemented')
 
 
 	@write_mode
 	@write_mode
-	async def import_address(self, addr, label, rescan=False): # rescan is True by default, so set to False
+	async def import_address(self, addr, *, label, rescan=False):
 		if (await self.rpc.walletinfo).get('descriptors'):
 		if (await self.rpc.walletinfo).get('descriptors'):
 			return await self.batch_import_address([(addr, label, rescan)])
 			return await self.batch_import_address([(addr, label, rescan)])
 		else:
 		else:

+ 1 - 1
mmgen/proto/btc/tw/rpc.py

@@ -64,7 +64,7 @@ class BitcoinTwRPC(TwRPC):
 		return [label_addr_pair(label, CoinAddr(self.proto, addrs[0]))
 		return [label_addr_pair(label, CoinAddr(self.proto, addrs[0]))
 			for label, addrs in zip(acct_labels, acct_addrs)]
 			for label, addrs in zip(acct_labels, acct_addrs)]
 
 
-	async def get_unspent_by_mmid(self, minconf=1, mmid_filter=[]):
+	async def get_unspent_by_mmid(self, *, minconf=1, mmid_filter=[]):
 		"""
 		"""
 		get unspent outputs in tracking wallet, compute balances per address
 		get unspent outputs in tracking wallet, compute balances per address
 		and return a dict with elements {'twmmid': {'addr', 'lbl', 'amt'}}
 		and return a dict with elements {'twmmid': {'addr', 'lbl', 'amt'}}

+ 10 - 10
mmgen/proto/btc/tw/txhistory.py

@@ -25,7 +25,7 @@ class BitcoinTwTransaction:
 
 
 	no_address_str = '[DATA]'
 	no_address_str = '[DATA]'
 
 
-	def __init__(self, parent, proto, rpc,
+	def __init__(self, *, parent, proto, rpc,
 			idx,          # unique numeric identifier of this transaction in listing
 			idx,          # unique numeric identifier of this transaction in listing
 			unspent_info, # addrs in wallet with balances: {'mmid': {'addr', 'comment', 'amt'}}
 			unspent_info, # addrs in wallet with balances: {'mmid': {'addr', 'comment', 'amt'}}
 			mm_map,       # all addrs in wallet: ['addr', ['twmmid', 'comment']]
 			mm_map,       # all addrs in wallet: ['addr', ['twmmid', 'comment']]
@@ -127,13 +127,13 @@ class BitcoinTwTransaction:
 		self.time = self.tx.get('blocktime') or self.tx['time']
 		self.time = self.tx.get('blocktime') or self.tx['time']
 		self.time_received = self.tx.get('timereceived')
 		self.time_received = self.tx.get('timereceived')
 
 
-	def blockheight_disp(self, color):
+	def blockheight_disp(self, *, color):
 		return (
 		return (
 			# old/altcoin daemons return no 'blockheight' field, so use confirmations instead
 			# old/altcoin daemons return no 'blockheight' field, so use confirmations instead
 			Int(self.rpc.blockcount + 1 - self.confirmations).hl(color=color)
 			Int(self.rpc.blockcount + 1 - self.confirmations).hl(color=color)
 			if self.confirmations > 0 else None)
 			if self.confirmations > 0 else None)
 
 
-	def age_disp(self, age_fmt, width, color):
+	def age_disp(self, age_fmt, *, width, color):
 		if age_fmt == 'confs':
 		if age_fmt == 'confs':
 			ret_str = str(self.confirmations).ljust(width)
 			ret_str = str(self.confirmations).ljust(width)
 			return gray(ret_str) if self.confirmations < 0 and color else ret_str
 			return gray(ret_str) if self.confirmations < 0 and color else ret_str
@@ -147,8 +147,8 @@ class BitcoinTwTransaction:
 	def txdate_disp(self, age_fmt):
 	def txdate_disp(self, age_fmt):
 		return self.parent.date_formatter[age_fmt](self.rpc, self.time)
 		return self.parent.date_formatter[age_fmt](self.rpc, self.time)
 
 
-	def txid_disp(self, color, width=None):
-		return self.txid.hl(color=color) if width is None else self.txid.truncate(width=width, color=color)
+	def txid_disp(self, *, color, width=None):
+		return self.txid.hl(color=color) if width is None else self.txid.truncate(width, color=color)
 
 
 	def vouts_list_disp(self, src, color, indent, addr_view_pref):
 	def vouts_list_disp(self, src, color, indent, addr_view_pref):
 
 
@@ -165,9 +165,9 @@ class BitcoinTwTransaction:
 						i = CoinTxID(e.txid).hl(color=color),
 						i = CoinTxID(e.txid).hl(color=color),
 						n = (nocolor, red)[color](str(e.data['n']).ljust(3)),
 						n = (nocolor, red)[color](str(e.data['n']).ljust(3)),
 						a = CoinAddr(self.proto, e.coin_addr).fmt(
 						a = CoinAddr(self.proto, e.coin_addr).fmt(
-							addr_view_pref, width=self.max_addrlen[src], color=color)
+							addr_view_pref, self.max_addrlen[src], color=color)
 								if e.coin_addr != self.no_address_str else
 								if e.coin_addr != self.no_address_str else
-							CoinAddr.fmtc(e.coin_addr, width=self.max_addrlen[src], color=color),
+							CoinAddr.fmtc(e.coin_addr, self.max_addrlen[src], color=color),
 						A = self.proto.coin_amt(e.data['value']).fmt(color=color)
 						A = self.proto.coin_amt(e.data['value']).fmt(color=color)
 					).rstrip()
 					).rstrip()
 				else:
 				else:
@@ -200,9 +200,9 @@ class BitcoinTwTransaction:
 					if width and space_left < addr_w:
 					if width and space_left < addr_w:
 						break
 						break
 					yield (
 					yield (
-						CoinAddr(self.proto, e.coin_addr).fmt(addr_view_pref, width=addr_w, color=color)
+						CoinAddr(self.proto, e.coin_addr).fmt(addr_view_pref, addr_w, color=color)
 							if e.coin_addr != self.no_address_str else
 							if e.coin_addr != self.no_address_str else
-						CoinAddr.fmtc(e.coin_addr, width=addr_w, color=color))
+						CoinAddr.fmtc(e.coin_addr, addr_w, color=color))
 					space_left -= addr_w
 					space_left -= addr_w
 				elif mmid.type == 'mmgen':
 				elif mmid.type == 'mmgen':
 					mmid_disp = mmid + bal_star
 					mmid_disp = mmid + bal_star
@@ -215,7 +215,7 @@ class BitcoinTwTransaction:
 						break
 						break
 					yield TwMMGenID.hl2(
 					yield TwMMGenID.hl2(
 						TwMMGenID,
 						TwMMGenID,
-						s = CoinAddr.fmtc(mmid.split(':', 1)[1] + bal_star, width=addr_w),
+						s = CoinAddr.fmtc(mmid.split(':', 1)[1] + bal_star, addr_w),
 						color = color,
 						color = color,
 						color_override = co)
 						color_override = co)
 					space_left -= addr_w
 					space_left -= addr_w

+ 3 - 3
mmgen/proto/btc/tx/base.py

@@ -75,7 +75,7 @@ def DeserializeTX(proto, txhex):
 	def bytes2coin_amt(bytes_le):
 	def bytes2coin_amt(bytes_le):
 		return proto.coin_amt(bytes2int(bytes_le), from_unit='satoshi')
 		return proto.coin_amt(bytes2int(bytes_le), from_unit='satoshi')
 
 
-	def bshift(n, skip=False, sub_null=False):
+	def bshift(n, *, skip=False, sub_null=False):
 		nonlocal idx, raw_tx
 		nonlocal idx, raw_tx
 		ret = tx[idx:idx+n]
 		ret = tx[idx:idx+n]
 		idx += n
 		idx += n
@@ -87,7 +87,7 @@ def DeserializeTX(proto, txhex):
 
 
 	# https://bitcoin.org/en/developer-reference#compactsize-unsigned-integers
 	# https://bitcoin.org/en/developer-reference#compactsize-unsigned-integers
 	# For example, the number 515 is encoded as 0xfd0302.
 	# For example, the number 515 is encoded as 0xfd0302.
-	def readVInt(skip=False):
+	def readVInt(*, skip=False):
 		nonlocal idx, raw_tx
 		nonlocal idx, raw_tx
 		s = tx[idx]
 		s = tx[idx]
 		idx += 1
 		idx += 1
@@ -292,7 +292,7 @@ class Base(TxBase):
 		return int(ret * (self.cfg.vsize_adj or 1))
 		return int(ret * (self.cfg.vsize_adj or 1))
 
 
 	# convert absolute CoinAmt fee to sat/byte for display using estimated size
 	# convert absolute CoinAmt fee to sat/byte for display using estimated size
-	def fee_abs2rel(self, abs_fee, to_unit='satoshi'):
+	def fee_abs2rel(self, abs_fee, *, to_unit='satoshi'):
 		return str(int(
 		return str(int(
 			abs_fee /
 			abs_fee /
 			getattr(self.proto.coin_amt, to_unit) /
 			getattr(self.proto.coin_amt, to_unit) /

+ 0 - 4
mmgen/proto/btc/tx/completed.py

@@ -75,10 +75,6 @@ class Completed(Base, TxBase.Completed):
 					'scriptPubKey:',          i.scriptPubKey,
 					'scriptPubKey:',          i.scriptPubKey,
 					'scriptPubKey->address:', ds.addr))
 					'scriptPubKey->address:', ds.addr))
 
 
-#	def is_replaceable_from_rpc(self):
-#		dec_tx = await self.rpc.call('decoderawtransaction', self.serialized)
-#		return None < dec_tx['vin'][0]['sequence'] <= self.proto.max_int - 2
-
 	def is_replaceable(self):
 	def is_replaceable(self):
 		return self.inputs[0].sequence == self.proto.max_int - 2
 		return self.inputs[0].sequence == self.proto.max_int - 2
 
 

+ 12 - 12
mmgen/proto/btc/tx/info.py

@@ -36,8 +36,8 @@ class TxInfo(TxInfo):
 			pink('{:0.6f}%'.format(tx.fee / tx.send_amt * 100))
 			pink('{:0.6f}%'.format(tx.fee / tx.send_amt * 100))
 		)
 		)
 
 
-	def format_abs_fee(self, color, iwidth):
-		return self.tx.fee.fmt(color=color, iwidth=iwidth)
+	def format_abs_fee(self, iwidth, /, *, color=None):
+		return self.tx.fee.fmt(iwidth, color=color)
 
 
 	def format_verbose_footer(self):
 	def format_verbose_footer(self):
 		tx = self.tx
 		tx = self.tx
@@ -48,7 +48,7 @@ class TxInfo(TxInfo):
 			out += f', Base {tsize-wsize}, Witness {wsize}'
 			out += f', Base {tsize-wsize}, Witness {wsize}'
 		return out + '\n'
 		return out + '\n'
 
 
-	def format_body(self, blockcount, nonmm_str, max_mmwid, enl, terse, sort):
+	def format_body(self, blockcount, nonmm_str, max_mmwid, enl, *, terse, sort):
 
 
 		if sort not in self.sort_orders:
 		if sort not in self.sort_orders:
 			die(1, '{!r}: invalid transaction view sort order. Valid options: {}'.format(
 			die(1, '{!r}: invalid transaction view sort order. Valid options: {}'.format(
@@ -58,15 +58,15 @@ class TxInfo(TxInfo):
 		def get_mmid_fmt(e, is_input):
 		def get_mmid_fmt(e, is_input):
 			if e.mmid:
 			if e.mmid:
 				return e.mmid.fmt2(
 				return e.mmid.fmt2(
-					width=max_mmwid,
-					encl='()',
-					color=True,
-					append_chars=('', ' (chg)')[bool(not is_input and e.is_chg and terse)],
-					append_color='green')
+					max_mmwid,
+					encl = '()',
+					color = True,
+					append_chars = ('', ' (chg)')[bool(not is_input and e.is_chg and terse)],
+					append_color = 'green')
 			else:
 			else:
 				return MMGenID.fmtc(
 				return MMGenID.fmtc(
 					'[vault address]' if not is_input and e.is_vault else nonmm_str,
 					'[vault address]' if not is_input and e.is_vault else nonmm_str,
-					width = max_mmwid,
+					max_mmwid,
 					color = True)
 					color = True)
 
 
 		def format_io(desc):
 		def format_io(desc):
@@ -91,9 +91,9 @@ class TxInfo(TxInfo):
 				for n, e in enumerate(io_sorted()):
 				for n, e in enumerate(io_sorted()):
 					yield '{:3} {} {} {} {}\n'.format(
 					yield '{:3} {} {} {} {}\n'.format(
 						n+1,
 						n+1,
-						e.addr.fmt(vp1, width=addr_w, color=True) if e.addr else blue(data_disp(e.data).ljust(addr_w)),
+						e.addr.fmt(vp1, addr_w, color=True) if e.addr else blue(data_disp(e.data).ljust(addr_w)),
 						get_mmid_fmt(e, is_input) if e.addr else ''.ljust(max_mmwid),
 						get_mmid_fmt(e, is_input) if e.addr else ''.ljust(max_mmwid),
-						e.amt.fmt(iwidth=iwidth, color=True),
+						e.amt.fmt(iwidth, color=True),
 						tx.dcoin)
 						tx.dcoin)
 					if have_bch and e.addr:
 					if have_bch and e.addr:
 						yield '{:3} [{}]\n'.format('', e.addr.hl(vp2, color=False))
 						yield '{:3} [{}]\n'.format('', e.addr.hl(vp2, color=False))
@@ -145,7 +145,7 @@ class TxInfo(TxInfo):
 			+ ''.join(format_io('inputs'))
 			+ ''.join(format_io('inputs'))
 			+ ''.join(format_io('outputs')))
 			+ ''.join(format_io('outputs')))
 
 
-	def strfmt_locktime(self, locktime=None, terse=False):
+	def strfmt_locktime(self, locktime=None, *, terse=False):
 		# Locktime itself is an unsigned 4-byte integer which can be parsed two ways:
 		# Locktime itself is an unsigned 4-byte integer which can be parsed two ways:
 		#
 		#
 		# If less than 500 million, locktime is parsed as a block height. The transaction can be
 		# If less than 500 million, locktime is parsed as a block height. The transaction can be

+ 10 - 5
mmgen/proto/btc/tx/new.py

@@ -22,6 +22,7 @@ class New(Base, TxNew):
 	usr_fee_prompt = 'Enter transaction fee: '
 	usr_fee_prompt = 'Enter transaction fee: '
 	fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})'
 	fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})'
 	no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
 	no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
+	msg_insufficient_funds = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
 
 
 	def process_data_output_arg(self, arg):
 	def process_data_output_arg(self, arg):
 		if any(arg.startswith(pfx) for pfx in ('data:', 'hexdata:')):
 		if any(arg.startswith(pfx) for pfx in ('data:', 'hexdata:')):
@@ -73,11 +74,11 @@ class New(Base, TxNew):
 		return fee_per_kb, fe_type
 		return fee_per_kb, fe_type
 
 
 	# given tx size, rel fee and units, return absolute fee
 	# given tx size, rel fee and units, return absolute fee
-	def fee_rel2abs(self, tx_size, units, amt_in_units, unit):
-		return self.proto.coin_amt(amt_in_units * tx_size, from_unit=units[unit])
+	def fee_rel2abs(self, tx_size, amt_in_units, unit):
+		return self.proto.coin_amt(int(amt_in_units * tx_size), from_unit=unit)
 
 
 	# given network fee estimate in BTC/kB, return absolute fee using estimated tx size
 	# given network fee estimate in BTC/kB, return absolute fee using estimated tx size
-	def fee_est2abs(self, fee_per_kb, fe_type=None):
+	def fee_est2abs(self, fee_per_kb, *, fe_type=None):
 		tx_size = self.estimate_size()
 		tx_size = self.estimate_size()
 		ret = self.proto.coin_amt('1') * (fee_per_kb * self.cfg.fee_adjust * tx_size / 1024)
 		ret = self.proto.coin_amt('1') * (fee_per_kb * self.cfg.fee_adjust * tx_size / 1024)
 		if self.cfg.verbose:
 		if self.cfg.verbose:
@@ -125,7 +126,11 @@ class New(Base, TxNew):
 	def final_inputs_ok_msg(self, funds_left):
 	def final_inputs_ok_msg(self, funds_left):
 		return 'Transaction produces {} {} in change'.format(funds_left.hl(), self.coin)
 		return 'Transaction produces {} {} in change'.format(funds_left.hl(), self.coin)
 
 
-	def check_chg_addr_is_wallet_addr(self, output=None, message='Change address is not an MMGen wallet address!'):
+	def check_chg_addr_is_wallet_addr(
+			self,
+			output  = None,
+			*,
+			message = 'Change address is not an MMGen wallet address!'):
 		def do_err():
 		def do_err():
 			from ....ui import confirm_or_raise
 			from ....ui import confirm_or_raise
 			confirm_or_raise(
 			confirm_or_raise(
@@ -138,7 +143,7 @@ class New(Base, TxNew):
 		elif len(self.nondata_outputs) > 1 and not self.chg_output.mmid:
 		elif len(self.nondata_outputs) > 1 and not self.chg_output.mmid:
 			do_err()
 			do_err()
 
 
-	async def create_serialized(self, locktime=None):
+	async def create_serialized(self, *, locktime=None):
 
 
 		if not self.is_bump:
 		if not self.is_bump:
 			# Set all sequence numbers to the same value, in conformity with the behavior of most modern wallets:
 			# Set all sequence numbers to the same value, in conformity with the behavior of most modern wallets:

+ 23 - 2
mmgen/proto/btc/tx/online.py

@@ -13,13 +13,13 @@ proto.btc.tx.online: Bitcoin online signed transaction class
 """
 """
 
 
 from ....tx import online as TxBase
 from ....tx import online as TxBase
-from ....util import msg, die
+from ....util import msg, ymsg, die
 from ....color import orange
 from ....color import orange
 from .signed import Signed
 from .signed import Signed
 
 
 class OnlineSigned(Signed, TxBase.OnlineSigned):
 class OnlineSigned(Signed, TxBase.OnlineSigned):
 
 
-	async def send(self, prompt_user=True):
+	async def send_checks(self):
 
 
 		self.check_correct_chain()
 		self.check_correct_chain()
 
 
@@ -37,6 +37,27 @@ class OnlineSigned(Signed, TxBase.OnlineSigned):
 
 
 		await self.status.display()
 		await self.status.display()
 
 
+	async def test_sendable(self):
+
+		await self.send_checks()
+
+		res = await self.rpc.call('testmempoolaccept', (self.serialized,))
+		ret = res[0]
+
+		if ret['allowed']:
+			from ....obj import CoinTxID
+			msg('TxID: {}'.format(CoinTxID(ret['txid']).hl()))
+			msg('Transaction can be sent')
+			return True
+		else:
+			ymsg('Transaction cannot be sent')
+			msg(ret['reject-reason'])
+			return False
+
+	async def send(self, *, prompt_user=True):
+
+		await self.send_checks()
+
 		if prompt_user:
 		if prompt_user:
 			self.confirm_send()
 			self.confirm_send()
 
 

+ 1 - 1
mmgen/proto/btc/tx/op_return_data.py

@@ -61,7 +61,7 @@ class OpReturnData(bytes, InitErrors):
 			self.display_hex = False
 			self.display_hex = False
 			return ret
 			return ret
 
 
-	def hl(self, add_label=False):
+	def hl(self, *, add_label=False):
 		'colorize and optionally label the result of str()'
 		'colorize and optionally label the result of str()'
 		from ....color import blue, pink
 		from ....color import blue, pink
 		ret = str(self)
 		ret = str(self)

+ 1 - 1
mmgen/proto/eth/addrdata.py

@@ -31,7 +31,7 @@ class EthereumTwAddrData(TwAddrData):
 		"""
 		"""
 	}
 	}
 
 
-	async def get_tw_data(self, twctl=None):
+	async def get_tw_data(self, *, twctl=None):
 		from ...tw.ctl import TwCtl
 		from ...tw.ctl import TwCtl
 		self.cfg._util.vmsg('Getting address data from tracking wallet')
 		self.cfg._util.vmsg('Getting address data from tracking wallet')
 		twctl = (twctl or await TwCtl(self.cfg, self.proto)).mmid_ordered_dict
 		twctl = (twctl or await TwCtl(self.cfg, self.proto)).mmid_ordered_dict

+ 7 - 4
mmgen/proto/eth/contract.py

@@ -45,7 +45,7 @@ class TokenCommon(MMGenObject):
 			int(parse_abi(data)[-1], 16) * self.base_unit,
 			int(parse_abi(data)[-1], 16) * self.base_unit,
 			from_decimal = True)
 			from_decimal = True)
 
 
-	async def do_call(self, method_sig, method_args='', toUnit=False):
+	async def do_call(self, method_sig, method_args='', *, toUnit=False):
 		data = self.create_method_id(method_sig) + method_args
 		data = self.create_method_id(method_sig) + method_args
 		if self.cfg.debug:
 		if self.cfg.debug:
 			msg('ETH_CALL {}:  {}'.format(
 			msg('ETH_CALL {}:  {}'.format(
@@ -99,6 +99,7 @@ class TokenCommon(MMGenObject):
 			self,
 			self,
 			to_addr,
 			to_addr,
 			amt,
 			amt,
+			*,
 			method_sig = 'transfer(address,uint256)'):
 			method_sig = 'transfer(address,uint256)'):
 		from_arg = ''
 		from_arg = ''
 		to_arg = to_addr.rjust(64, '0')
 		to_arg = to_addr.rjust(64, '0')
@@ -112,6 +113,7 @@ class TokenCommon(MMGenObject):
 			start_gas,
 			start_gas,
 			gasPrice,
 			gasPrice,
 			nonce,
 			nonce,
+			*,
 			method_sig = 'transfer(address,uint256)'):
 			method_sig = 'transfer(address,uint256)'):
 		data = self.create_data(
 		data = self.create_data(
 				to_addr,
 				to_addr,
@@ -125,7 +127,7 @@ class TokenCommon(MMGenObject):
 			'nonce':    nonce,
 			'nonce':    nonce,
 			'data':     bytes.fromhex(data)}
 			'data':     bytes.fromhex(data)}
 
 
-	async def txsign(self, tx_in, key, from_addr, chain_id=None):
+	async def txsign(self, tx_in, key, from_addr, *, chain_id=None):
 
 
 		from .pyethereum.transactions import Transaction
 		from .pyethereum.transactions import Transaction
 
 
@@ -162,6 +164,7 @@ class TokenCommon(MMGenObject):
 			key,
 			key,
 			start_gas,
 			start_gas,
 			gasPrice,
 			gasPrice,
+			*,
 			method_sig = 'transfer(address,uint256)'):
 			method_sig = 'transfer(address,uint256)'):
 		tx_in = self.make_tx_in(
 		tx_in = self.make_tx_in(
 				to_addr,
 				to_addr,
@@ -175,7 +178,7 @@ class TokenCommon(MMGenObject):
 
 
 class Token(TokenCommon):
 class Token(TokenCommon):
 
 
-	def __init__(self, cfg, proto, addr, decimals, rpc=None):
+	def __init__(self, cfg, proto, addr, decimals, *, rpc=None):
 		if type(self).__name__ == 'Token':
 		if type(self).__name__ == 'Token':
 			from ...util2 import get_keccak
 			from ...util2 import get_keccak
 			self.keccak_256 = get_keccak(cfg)
 			self.keccak_256 = get_keccak(cfg)
@@ -199,4 +202,4 @@ class ResolvedToken(TokenCommon, metaclass=AsyncInit):
 		decimals = await self.get_decimals() # requires self.addr!
 		decimals = await self.get_decimals() # requires self.addr!
 		if not decimals:
 		if not decimals:
 			die('TokenNotInBlockchain', f'Token {addr!r} not in blockchain')
 			die('TokenNotInBlockchain', f'Token {addr!r} not in blockchain')
-		Token.__init__(self, cfg, proto, addr, decimals, rpc)
+		Token.__init__(self, cfg, proto, addr, decimals, rpc=rpc)

+ 3 - 3
mmgen/proto/eth/daemon.py

@@ -174,12 +174,12 @@ class erigon_daemon(geth_daemon):
 			test_suite   = self.test_suite,
 			test_suite   = self.test_suite,
 			datadir      = self.datadir)
 			datadir      = self.datadir)
 
 
-	def start(self, quiet=False, silent=False):
+	def start(self, *, quiet=False, silent=False):
 		super().start(quiet=quiet, silent=silent)
 		super().start(quiet=quiet, silent=silent)
 		self.rpc_d.debug = self.debug
 		self.rpc_d.debug = self.debug
 		return self.rpc_d.start(quiet=quiet, silent=silent)
 		return self.rpc_d.start(quiet=quiet, silent=silent)
 
 
-	def stop(self, quiet=False, silent=False):
+	def stop(self, *, quiet=False, silent=False):
 		self.rpc_d.debug = self.debug
 		self.rpc_d.debug = self.debug
 		self.rpc_d.stop(quiet=quiet, silent=silent)
 		self.rpc_d.stop(quiet=quiet, silent=silent)
 		return super().stop(quiet=quiet, silent=silent)
 		return super().stop(quiet=quiet, silent=silent)
@@ -196,7 +196,7 @@ class erigon_rpcdaemon(RPCDaemon):
 	use_pidfile = False
 	use_pidfile = False
 	use_threads = True
 	use_threads = True
 
 
-	def __init__(self, cfg, proto, rpc_port, private_port, test_suite, datadir):
+	def __init__(self, cfg, proto, *, rpc_port, private_port, test_suite, datadir):
 
 
 		self.proto = proto
 		self.proto = proto
 		self.test_suite = test_suite
 		self.test_suite = test_suite

+ 1 - 1
mmgen/proto/eth/misc.py

@@ -14,7 +14,7 @@ proto.eth.misc: miscellaneous utilities for Ethereum base protocol
 
 
 from ...util2 import get_keccak
 from ...util2 import get_keccak
 
 
-def decrypt_geth_keystore(cfg, wallet_fn, passwd, check_addr=True):
+def decrypt_geth_keystore(cfg, wallet_fn, passwd, *, check_addr=True):
 	"""
 	"""
 	Decrypt the encrypted private key in a Geth keystore wallet, returning the decrypted key
 	Decrypt the encrypted private key in a Geth keystore wallet, returning the decrypted key
 	"""
 	"""

+ 1 - 0
mmgen/proto/eth/params.py

@@ -55,6 +55,7 @@ class mainnet(CoinProtocol.DummyWIF, CoinProtocol.Secp256k1):
 	}
 	}
 
 
 	coin_cfg_opts = (
 	coin_cfg_opts = (
+		'daemon_id',
 		'ignore_daemon_version',
 		'ignore_daemon_version',
 		'rpc_host',
 		'rpc_host',
 		'rpc_port',
 		'rpc_port',

+ 5 - 7
mmgen/proto/eth/rpc.py

@@ -25,10 +25,6 @@ class daemon_warning(oneshot_warning_group):
 		color = 'yellow'
 		color = 'yellow'
 		message = 'Geth has not been tested on mainnet. You may experience problems.'
 		message = 'Geth has not been tested on mainnet. You may experience problems.'
 
 
-	class reth:
-		color = 'yellow'
-		message = 'Reth has not been tested on mainnet. You may experience problems.'
-
 	class erigon:
 	class erigon:
 		color = 'red'
 		color = 'red'
 		message = 'Erigon support is EXPERIMENTAL. Use at your own risk!!!'
 		message = 'Erigon support is EXPERIMENTAL. Use at your own risk!!!'
@@ -42,6 +38,7 @@ class EthereumRPCClient(RPCClient, metaclass=AsyncInit):
 			self,
 			self,
 			cfg,
 			cfg,
 			proto,
 			proto,
+			*,
 			daemon,
 			daemon,
 			backend,
 			backend,
 			ignore_wallet):
 			ignore_wallet):
@@ -82,13 +79,14 @@ class EthereumRPCClient(RPCClient, metaclass=AsyncInit):
 		if self.daemon.id in ('parity', 'openethereum'):
 		if self.daemon.id in ('parity', 'openethereum'):
 			if (await self.call('parity_nodeKind'))['capability'] == 'full':
 			if (await self.call('parity_nodeKind'))['capability'] == 'full':
 				self.caps += ('full_node',)
 				self.caps += ('full_node',)
-			self.chainID = None if ci is None else Int(ci, 16) # parity/oe return chainID only for dev chain
+			# parity/openethereum return chainID only for dev chain:
+			self.chainID = None if ci is None else Int(ci, base=16)
 			self.chain = (await self.call('parity_chain')).replace(' ', '_').replace('_testnet', '')
 			self.chain = (await self.call('parity_chain')).replace(' ', '_').replace('_testnet', '')
 		elif self.daemon.id in ('geth', 'reth', 'erigon'):
 		elif self.daemon.id in ('geth', 'reth', 'erigon'):
-			if self.daemon.network == 'mainnet':
+			if self.daemon.network == 'mainnet' and hasattr(daemon_warning, self.daemon.id):
 				daemon_warning(self.daemon.id)
 				daemon_warning(self.daemon.id)
 			self.caps += ('full_node',)
 			self.caps += ('full_node',)
-			self.chainID = Int(ci, 16)
+			self.chainID = Int(ci, base=16)
 			self.chain = self.proto.chain_ids[self.chainID]
 			self.chain = self.proto.chain_ids[self.chainID]
 
 
 	def make_host_path(self, wallet):
 	def make_host_path(self, wallet):

+ 1 - 1
mmgen/proto/eth/tw/addresses.py

@@ -37,7 +37,7 @@ class EthereumTwAddresses(TwAddresses, EthereumTwView, EthereumTwRPC):
 		'w':'a_view_detail',
 		'w':'a_view_detail',
 		'p':'a_print_detail'}
 		'p':'a_print_detail'}
 
 
-	def get_column_widths(self, data, wide, interactive):
+	def get_column_widths(self, data, *, wide, interactive):
 
 
 		return self.compute_column_widths(
 		return self.compute_column_widths(
 			widths = { # fixed cols
 			widths = { # fixed cols

+ 2 - 2
mmgen/proto/eth/tw/bal.py

@@ -30,9 +30,9 @@ class EthereumTwGetBalance(TwGetBalance):
 		'ge_minconf': 'Balance',
 		'ge_minconf': 'Balance',
 	}
 	}
 
 
-	async def __init__(self, cfg, proto, *args, **kwargs):
+	async def __init__(self, cfg, proto, *, minconf, quiet):
 		self.twctl = await TwCtl(cfg, proto, mode='w')
 		self.twctl = await TwCtl(cfg, proto, mode='w')
-		await super().__init__(cfg, proto, *args, **kwargs)
+		await super().__init__(cfg, proto, minconf=minconf, quiet=quiet)
 
 
 	async def create_data(self):
 	async def create_data(self):
 		in_data = self.twctl.mmid_ordered_dict
 		in_data = self.twctl.mmid_ordered_dict

+ 7 - 6
mmgen/proto/eth/tw/ctl.py

@@ -88,13 +88,13 @@ class EthereumTwCtl(TwCtl):
 
 
 	@write_mode
 	@write_mode
 	async def batch_import_address(self, args_list):
 	async def batch_import_address(self, args_list):
-		return [await self.import_address(*a) for a in args_list]
+		return [await self.import_address(a, label=b, rescan=c) for a, b, c in args_list]
 
 
 	async def rescan_addresses(self, coin_addrs):
 	async def rescan_addresses(self, coin_addrs):
 		pass
 		pass
 
 
 	@write_mode
 	@write_mode
-	async def import_address(self, addr, label, rescan=False):
+	async def import_address(self, addr, *, label, rescan=False):
 		r = self.data_root
 		r = self.data_root
 		if addr in r:
 		if addr in r:
 			if not r[addr]['mmid'] and label.mmid:
 			if not r[addr]['mmid'] and label.mmid:
@@ -174,7 +174,7 @@ class EthereumTokenTwCtl(EthereumTwCtl):
 	symbol = None
 	symbol = None
 	cur_eth_balances = {}
 	cur_eth_balances = {}
 
 
-	async def __init__(self, cfg, proto, mode='r', token_addr=None, no_rpc=False):
+	async def __init__(self, cfg, proto, *, mode='r', token_addr=None, no_rpc=False):
 
 
 		await super().__init__(cfg, proto, mode=mode, no_rpc=no_rpc)
 		await super().__init__(cfg, proto, mode=mode, no_rpc=no_rpc)
 
 
@@ -213,15 +213,16 @@ class EthereumTokenTwCtl(EthereumTwCtl):
 		return 'token ' + self.get_param('symbol')
 		return 'token ' + self.get_param('symbol')
 
 
 	async def rpc_get_balance(self, addr):
 	async def rpc_get_balance(self, addr):
-		return await Token(self.cfg, self.proto, self.token, self.decimals, self.rpc).get_balance(addr)
+		return await Token(
+			self.cfg, self.proto, self.token, self.decimals, rpc=self.rpc).get_balance(addr)
 
 
-	async def get_eth_balance(self, addr, force_rpc=False):
+	async def get_eth_balance(self, addr, *, force_rpc=False):
 		cache = self.cur_eth_balances
 		cache = self.cur_eth_balances
 		r = self.data['accounts']
 		r = self.data['accounts']
 		ret = None if force_rpc else self.get_cached_balance(addr, cache, r)
 		ret = None if force_rpc else self.get_cached_balance(addr, cache, r)
 		if ret is None:
 		if ret is None:
 			ret = await super().rpc_get_balance(addr)
 			ret = await super().rpc_get_balance(addr)
-			self.cache_balance(addr, ret, cache, r)
+			self.cache_balance(addr, ret, session_cache=cache, data_root=r)
 		return ret
 		return ret
 
 
 	def get_param(self, param):
 	def get_param(self, param):

+ 1 - 1
mmgen/proto/eth/tw/json.py

@@ -109,7 +109,7 @@ class EthereumTwJSON(TwJSON):
 
 
 	class Export(TwJSON.Export, Base):
 	class Export(TwJSON.Export, Base):
 
 
-		async def get_entries(self, include_amts=True):
+		async def get_entries(self, *, include_amts=True):
 
 
 			def gen_data(data):
 			def gen_data(data):
 				for k, v in data.items():
 				for k, v in data.items():

+ 2 - 2
mmgen/proto/eth/tw/unspent.py

@@ -66,7 +66,7 @@ class EthereumTwUnspentOutputs(EthereumTwView, TwUnspentOutputs):
 
 
 	no_data_errmsg = 'No accounts in tracking wallet!'
 	no_data_errmsg = 'No accounts in tracking wallet!'
 
 
-	def get_column_widths(self, data, wide, interactive):
+	def get_column_widths(self, data, *, wide, interactive):
 		# min screen width: 80 cols
 		# min screen width: 80 cols
 		# num addr [mmid] [comment] amt [amt2]
 		# num addr [mmid] [comment] amt [amt2]
 		return self.compute_column_widths(
 		return self.compute_column_widths(
@@ -95,7 +95,7 @@ class EthereumTwUnspentOutputs(EthereumTwView, TwUnspentOutputs):
 			interactive = interactive,
 			interactive = interactive,
 		)
 		)
 
 
-	def do_sort(self, key=None, reverse=False):
+	def do_sort(self, key=None, *, reverse=False):
 		if key == 'txid':
 		if key == 'txid':
 			return
 			return
 		super().do_sort(key=key, reverse=reverse)
 		super().do_sort(key=key, reverse=reverse)

+ 9 - 10
mmgen/proto/eth/tx/base.py

@@ -34,18 +34,17 @@ class Base(TxBase.Base):
 		return self.outputs
 		return self.outputs
 
 
 	def pretty_fmt_fee(self, fee):
 	def pretty_fmt_fee(self, fee):
-		if fee < 1:
-			ret = f'{fee:.8f}'.rstrip('0')
-			return ret + '0' if ret.endswith('.') else ret
+		if fee < 10:
+			return f'{fee:.3f}'.rstrip('0').rstrip('.')
 		return str(int(fee))
 		return str(int(fee))
 
 
 	# given absolute fee in ETH, return gas price for display in selected unit
 	# given absolute fee in ETH, return gas price for display in selected unit
-	def fee_abs2rel(self, abs_fee, to_unit='Gwei'):
+	def fee_abs2rel(self, abs_fee, *, to_unit='Gwei'):
 		return self.pretty_fmt_fee(
 		return self.pretty_fmt_fee(
-			self.fee_abs2gas(abs_fee).to_unit(to_unit))
+			self.fee_abs2gasprice(abs_fee).to_unit(to_unit))
 
 
 	# given absolute fee in ETH, return gas price in ETH
 	# given absolute fee in ETH, return gas price in ETH
-	def fee_abs2gas(self, abs_fee):
+	def fee_abs2gasprice(self, abs_fee):
 		return self.proto.coin_amt(int(abs_fee.toWei() // self.gas.toWei()), from_unit='wei')
 		return self.proto.coin_amt(int(abs_fee.toWei() // self.gas.toWei()), from_unit='wei')
 
 
 	# given rel fee (gasPrice) in wei, return absolute fee using self.gas (Ethereum-only method)
 	# given rel fee (gasPrice) in wei, return absolute fee using self.gas (Ethereum-only method)
@@ -63,10 +62,10 @@ class Base(TxBase.Base):
 		tx = await self.rpc.call('eth_getTransactionByHash', '0x'+txid)
 		tx = await self.rpc.call('eth_getTransactionByHash', '0x'+txid)
 		return namedtuple('exec_status',
 		return namedtuple('exec_status',
 				['status', 'gas_sent', 'gas_used', 'gas_price', 'contract_addr', 'tx', 'rx'])(
 				['status', 'gas_sent', 'gas_used', 'gas_price', 'contract_addr', 'tx', 'rx'])(
-			status        = Int(rx['status'], 16), # zero is failure, non-zero success
-			gas_sent      = Int(tx['gas'], 16),
-			gas_used      = Int(rx['gasUsed'], 16),
-			gas_price     = self.proto.coin_amt(int(tx['gasPrice'], 16), from_unit='wei'),
+			status        = Int(rx['status'], base=16), # zero is failure, non-zero success
+			gas_sent      = Int(tx['gas'], base=16),
+			gas_used      = Int(rx['gasUsed'], base=16),
+			gas_price     = self.proto.coin_amt(Int(tx['gasPrice'], base=16), from_unit='wei'),
 			contract_addr = self.proto.coin_addr(rx['contractAddress'][2:])
 			contract_addr = self.proto.coin_addr(rx['contractAddress'][2:])
 				if rx['contractAddress'] else None,
 				if rx['contractAddress'] else None,
 			tx = tx,
 			tx = tx,

+ 1 - 1
mmgen/proto/eth/tx/bump.py

@@ -29,7 +29,7 @@ class Bump(Completed, New, TxBase.Bump):
 		return self.fee * Decimal('1.101')
 		return self.fee * Decimal('1.101')
 
 
 	def bump_fee(self, idx, fee):
 	def bump_fee(self, idx, fee):
-		self.txobj['gasPrice'] = self.fee_abs2gas(fee)
+		self.txobj['gasPrice'] = self.fee_abs2gasprice(fee)
 
 
 	async def get_nonce(self):
 	async def get_nonce(self):
 		return self.txobj['nonce']
 		return self.txobj['nonce']

+ 3 - 3
mmgen/proto/eth/tx/info.py

@@ -28,7 +28,7 @@ class TxInfo(TxInfo):
 	""")
 	""")
 	to_addr_key = 'to'
 	to_addr_key = 'to'
 
 
-	def format_body(self, blockcount, nonmm_str, max_mmwid, enl, terse, sort):
+	def format_body(self, blockcount, nonmm_str, max_mmwid, enl, *, terse, sort):
 		tx = self.tx
 		tx = self.tx
 		m = {}
 		m = {}
 		for k in ('inputs', 'outputs'):
 		for k in ('inputs', 'outputs'):
@@ -59,8 +59,8 @@ class TxInfo(TxInfo):
 			t_mmid = m['outputs'] if len(tx.outputs) else '',
 			t_mmid = m['outputs'] if len(tx.outputs) else '',
 			f_mmid = m['inputs']) + '\n\n'
 			f_mmid = m['inputs']) + '\n\n'
 
 
-	def format_abs_fee(self, color, iwidth):
-		return self.tx.fee.fmt(color=color, iwidth=iwidth) + (' (max)' if self.tx.txobj['data'] else '')
+	def format_abs_fee(self, iwidth, /, *, color=None):
+		return self.tx.fee.fmt(iwidth, color=color) + (' (max)' if self.tx.txobj['data'] else '')
 
 
 	def format_rel_fee(self):
 	def format_rel_fee(self):
 		return ' ({} of spend amount)'.format(
 		return ' ({} of spend amount)'.format(

+ 7 - 8
mmgen/proto/eth/tx/new.py

@@ -55,17 +55,16 @@ class New(Base, TxBase.New):
 			'from': self.inputs[0].addr,
 			'from': self.inputs[0].addr,
 			'to':   self.outputs[0].addr if self.outputs else None,
 			'to':   self.outputs[0].addr if self.outputs else None,
 			'amt':  self.outputs[0].amt if self.outputs else self.proto.coin_amt('0'),
 			'amt':  self.outputs[0].amt if self.outputs else self.proto.coin_amt('0'),
-			'gasPrice': self.fee_abs2gas(self.usr_fee),
+			'gasPrice': self.fee_abs2gasprice(self.usr_fee),
 			'startGas': self.start_gas,
 			'startGas': self.start_gas,
 			'nonce': await self.get_nonce(),
 			'nonce': await self.get_nonce(),
 			'chainId': self.rpc.chainID,
 			'chainId': self.rpc.chainID,
-			'data':  self.usr_contract_data,
-		}
+			'data':  self.usr_contract_data}
 
 
 	# Instead of serializing tx data as with BTC, just create a JSON dump.
 	# Instead of serializing tx data as with BTC, just create a JSON dump.
 	# This complicates things but means we avoid using the rlp library to deserialize the data,
 	# This complicates things but means we avoid using the rlp library to deserialize the data,
 	# thus removing an attack vector
 	# thus removing an attack vector
-	async def create_serialized(self, locktime=None):
+	async def create_serialized(self, *, locktime=None):
 		assert len(self.inputs) == 1, 'Transaction has more than one input!'
 		assert len(self.inputs) == 1, 'Transaction has more than one input!'
 		o_num = len(self.outputs)
 		o_num = len(self.outputs)
 		o_ok = 0 if self.usr_contract_data else 1
 		o_ok = 0 if self.usr_contract_data else 1
@@ -117,7 +116,7 @@ class New(Base, TxBase.New):
 
 
 	# get rel_fee (gas price) from network, return in native wei
 	# get rel_fee (gas price) from network, return in native wei
 	async def get_rel_fee_from_network(self):
 	async def get_rel_fee_from_network(self):
-		return Int(await self.rpc.call('eth_gasPrice'), 16), 'eth_gasPrice'
+		return Int(await self.rpc.call('eth_gasPrice'), base=16), 'eth_gasPrice'
 
 
 	def check_chg_addr_is_wallet_addr(self):
 	def check_chg_addr_is_wallet_addr(self):
 		pass
 		pass
@@ -127,11 +126,11 @@ class New(Base, TxBase.New):
 			assert self.usr_fee <= self.proto.max_tx_fee
 			assert self.usr_fee <= self.proto.max_tx_fee
 
 
 	# given rel fee and units, return absolute fee using self.gas
 	# given rel fee and units, return absolute fee using self.gas
-	def fee_rel2abs(self, tx_size, units, amt_in_units, unit):
-		return self.proto.coin_amt(amt_in_units, from_unit=units[unit]) * self.gas.toWei()
+	def fee_rel2abs(self, tx_size, amt_in_units, unit):
+		return self.proto.coin_amt(int(amt_in_units * self.gas.toWei()), from_unit=unit)
 
 
 	# given fee estimate (gas price) in wei, return absolute fee, adjusting by self.cfg.fee_adjust
 	# given fee estimate (gas price) in wei, return absolute fee, adjusting by self.cfg.fee_adjust
-	def fee_est2abs(self, rel_fee, fe_type=None):
+	def fee_est2abs(self, rel_fee, *, fe_type=None):
 		ret = self.fee_gasPrice2abs(rel_fee) * self.cfg.fee_adjust
 		ret = self.fee_gasPrice2abs(rel_fee) * self.cfg.fee_adjust
 		if self.cfg.verbose:
 		if self.cfg.verbose:
 			msg(f'Estimated fee: {ret} ETH')
 			msg(f'Estimated fee: {ret} ETH')

+ 4 - 1
mmgen/proto/eth/tx/online.py

@@ -20,7 +20,10 @@ from .signed import Signed, TokenSigned
 
 
 class OnlineSigned(Signed, TxBase.OnlineSigned):
 class OnlineSigned(Signed, TxBase.OnlineSigned):
 
 
-	async def send(self, prompt_user=True):
+	async def test_sendable(self):
+		raise NotImplementedError('transaction testing not implemented for Ethereum')
+
+	async def send(self, *, prompt_user=True):
 
 
 		self.check_correct_chain()
 		self.check_correct_chain()
 
 

+ 2 - 2
mmgen/proto/secp256k1/keygen.py

@@ -37,7 +37,7 @@ class backend:
 				compressed = privkey.compressed)
 				compressed = privkey.compressed)
 
 
 		@classmethod
 		@classmethod
-		def get_clsname(cls, cfg, silent=False):
+		def get_clsname(cls, cfg, *, silent=False):
 			try:
 			try:
 				from .secp256k1 import pubkey_gen
 				from .secp256k1 import pubkey_gen
 				if not pubkey_gen(bytes.fromhex('deadbeef'*8), 1):
 				if not pubkey_gen(bytes.fromhex('deadbeef'*8), 1):
@@ -67,7 +67,7 @@ class backend:
 			Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or
 			Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or
 			0x02 depending on whether they're greater or less than the midpoint of the curve.
 			0x02 depending on whether they're greater or less than the midpoint of the curve.
 			"""
 			"""
-			def privnum2pubkey(numpriv, compressed=False):
+			def privnum2pubkey(numpriv, *, compressed=False):
 				pk = self.ecdsa.SigningKey.from_secret_exponent(numpriv, curve=self.ecdsa.SECP256k1)
 				pk = self.ecdsa.SigningKey.from_secret_exponent(numpriv, curve=self.ecdsa.SECP256k1)
 				# vk_bytes = x (32 bytes) + y (32 bytes) (unsigned big-endian)
 				# vk_bytes = x (32 bytes) + y (32 bytes) (unsigned big-endian)
 				return pubkey_format(pk.verifying_key.to_string(), compressed)
 				return pubkey_format(pk.verifying_key.to_string(), compressed)

+ 1 - 0
mmgen/proto/xmr/daemon.py

@@ -104,6 +104,7 @@ class MoneroWalletDaemon(RPCDaemon):
 			self,
 			self,
 			cfg,
 			cfg,
 			proto,
 			proto,
+			*,
 			wallet_dir  = None,
 			wallet_dir  = None,
 			test_suite  = False,
 			test_suite  = False,
 			user        = None,
 			user        = None,

+ 5 - 4
mmgen/proto/xmr/rpc.py

@@ -25,6 +25,7 @@ class MoneroRPCClient(RPCClient):
 			self,
 			self,
 			cfg,
 			cfg,
 			proto,
 			proto,
+			*,
 			host,
 			host,
 			port,
 			port,
 			user,
 			user,
@@ -42,7 +43,7 @@ class MoneroRPCClient(RPCClient):
 			if host.endswith('.onion'):
 			if host.endswith('.onion'):
 				self.network_proto = 'http'
 				self.network_proto = 'http'
 
 
-		super().__init__(cfg, host, port, test_connection)
+		super().__init__(cfg, host, port, test_connection=test_connection)
 
 
 		if self.auth_type:
 		if self.auth_type:
 			self.auth = auth_data(user, passwd)
 			self.auth = auth_data(user, passwd)
@@ -90,7 +91,7 @@ class MoneroRPCClient(RPCClient):
 			host_path = f'/{method}'
 			host_path = f'/{method}'
 		), json_rpc=False)
 		), json_rpc=False)
 
 
-	async def do_stop_daemon(self, silent=False):
+	async def do_stop_daemon(self, *, silent=False):
 		return self.call_raw('stop_daemon') # unreliable on macOS (daemon stops, but closes connection)
 		return self.call_raw('stop_daemon') # unreliable on macOS (daemon stops, but closes connection)
 
 
 	rpcmethods = ('get_info',)
 	rpcmethods = ('get_info',)
@@ -100,7 +101,7 @@ class MoneroWalletRPCClient(MoneroRPCClient):
 
 
 	auth_type = 'digest'
 	auth_type = 'digest'
 
 
-	def __init__(self, cfg, daemon, test_connection=True):
+	def __init__(self, cfg, daemon, *, test_connection=True):
 
 
 		RPCClient.__init__(
 		RPCClient.__init__(
 			self            = self,
 			self            = self,
@@ -128,7 +129,7 @@ class MoneroWalletRPCClient(MoneroRPCClient):
 	def call_raw(self, *args, **kwargs):
 	def call_raw(self, *args, **kwargs):
 		raise NotImplementedError('call_raw() not implemented for class MoneroWalletRPCClient')
 		raise NotImplementedError('call_raw() not implemented for class MoneroWalletRPCClient')
 
 
-	async def do_stop_daemon(self, silent=False):
+	async def do_stop_daemon(self, *, silent=False):
 		"""
 		"""
 		NB: the 'stop_wallet' RPC call closes the open wallet before shutting down the daemon,
 		NB: the 'stop_wallet' RPC call closes the open wallet before shutting down the daemon,
 		returning an error if no wallet is open
 		returning an error if no wallet is open

+ 4 - 2
mmgen/protocol.py

@@ -59,7 +59,7 @@ class CoinProtocol(MMGenObject):
 		decimal_prec = 28
 		decimal_prec = 28
 		_set_ok = ('tokensym',)
 		_set_ok = ('tokensym',)
 
 
-		def __init__(self, cfg, coin, name, network, tokensym=None, need_amt=False):
+		def __init__(self, cfg, coin, *, name, network, tokensym=None, need_amt=False):
 			self.cfg        = cfg
 			self.cfg        = cfg
 			self.coin       = coin.upper()
 			self.coin       = coin.upper()
 			self.coin_id    = self.coin
 			self.coin_id    = self.coin
@@ -180,7 +180,7 @@ class CoinProtocol(MMGenObject):
 		def viewkey(self, viewkey_str):
 		def viewkey(self, viewkey_str):
 			raise NotImplementedError(f'{self.name} protocol does not support view keys')
 			raise NotImplementedError(f'{self.name} protocol does not support view keys')
 
 
-		def base_proto_subclass(self, cls, modname, sub_clsname=None, is_token=False):
+		def base_proto_subclass(self, cls, modname, *, sub_clsname=None, is_token=False):
 			"""
 			"""
 			magic module loading and class selection
 			magic module loading and class selection
 			"""
 			"""
@@ -212,6 +212,7 @@ class CoinProtocol(MMGenObject):
 		rpc_user              = ''
 		rpc_user              = ''
 		rpc_password          = ''
 		rpc_password          = ''
 		tw_name               = ''
 		tw_name               = ''
+		daemon_id             = ''
 
 
 		@classmethod
 		@classmethod
 		def get_opt_clsval(cls, cfg, opt):
 		def get_opt_clsval(cls, cfg, opt):
@@ -281,6 +282,7 @@ class CoinProtocol(MMGenObject):
 def init_proto(
 def init_proto(
 		cfg,
 		cfg,
 		coin       = None,
 		coin       = None,
+		*,
 		testnet    = False,
 		testnet    = False,
 		regtest    = False,
 		regtest    = False,
 		network    = None,
 		network    = None,

+ 10 - 9
mmgen/rpc.py

@@ -30,7 +30,7 @@ from .objmethods import HiliteStr, InitErrors, MMGenObject
 
 
 auth_data = namedtuple('rpc_auth_data', ['user', 'passwd'])
 auth_data = namedtuple('rpc_auth_data', ['user', 'passwd'])
 
 
-def dmsg_rpc(fs, data=None, is_json=False):
+def dmsg_rpc(fs, data=None, *, is_json=False):
 	msg(
 	msg(
 		fs if data is None else
 		fs if data is None else
 		fs.format(pp_fmt(json.loads(data) if is_json else data))
 		fs.format(pp_fmt(json.loads(data) if is_json else data))
@@ -255,7 +255,7 @@ class RPCClient(MMGenObject):
 	network_proto = 'http'
 	network_proto = 'http'
 	proxy = None
 	proxy = None
 
 
-	def __init__(self, cfg, host, port, test_connection=True):
+	def __init__(self, cfg, host, port, *, test_connection=True):
 
 
 		self.cfg = cfg
 		self.cfg = cfg
 		self.name = type(self).__name__
 		self.name = type(self).__name__
@@ -317,7 +317,7 @@ class RPCClient(MMGenObject):
 			host_path = self.make_host_path(wallet)
 			host_path = self.make_host_path(wallet)
 		))
 		))
 
 
-	async def batch_call(self, method, param_list, timeout=None, wallet=None):
+	async def batch_call(self, method, param_list, *, timeout=None, wallet=None):
 		"""
 		"""
 		Make a single call with a list of tuples as first argument
 		Make a single call with a list of tuples as first argument
 		For RPC calls that return a list of results
 		For RPC calls that return a list of results
@@ -332,7 +332,7 @@ class RPCClient(MMGenObject):
 			host_path = self.make_host_path(wallet)
 			host_path = self.make_host_path(wallet)
 		), batch=True)
 		), batch=True)
 
 
-	async def gathered_call(self, method, args_list, timeout=None, wallet=None):
+	async def gathered_call(self, method, args_list, *, timeout=None, wallet=None):
 		"""
 		"""
 		Perform multiple RPC calls, returning results in a list
 		Perform multiple RPC calls, returning results in a list
 		Can be called two ways:
 		Can be called two ways:
@@ -369,14 +369,14 @@ class RPCClient(MMGenObject):
 			timeout = timeout,
 			timeout = timeout,
 			wallet = wallet)
 			wallet = wallet)
 
 
-	def gathered_icall(self, method, args_list, timeout=None, wallet=None):
+	def gathered_icall(self, method, args_list, *, timeout=None, wallet=None):
 		return self.gathered_call(
 		return self.gathered_call(
 			method,
 			method,
 			[getattr(self.call_sigs, method)(*a)[1:] for a in args_list],
 			[getattr(self.call_sigs, method)(*a)[1:] for a in args_list],
 			timeout = timeout,
 			timeout = timeout,
 			wallet = wallet)
 			wallet = wallet)
 
 
-	def process_http_resp(self, run_ret, batch=False, json_rpc=True):
+	def process_http_resp(self, run_ret, *, batch=False, json_rpc=True):
 
 
 		def float_parser(n):
 		def float_parser(n):
 			return n
 			return n
@@ -424,7 +424,7 @@ class RPCClient(MMGenObject):
 						m = text
 						m = text
 			die('RPCFailure', f'{s.value} {s.name}: {m}')
 			die('RPCFailure', f'{s.value} {s.name}: {m}')
 
 
-	async def stop_daemon(self, quiet=False, silent=False):
+	async def stop_daemon(self, *, quiet=False, silent=False):
 		if self.daemon.state == 'ready':
 		if self.daemon.state == 'ready':
 			if not (quiet or silent):
 			if not (quiet or silent):
 				msg(f'Stopping {self.daemon.desc} on port {self.daemon.bind_port}')
 				msg(f'Stopping {self.daemon.desc} on port {self.daemon.bind_port}')
@@ -437,10 +437,10 @@ class RPCClient(MMGenObject):
 				msg(f'{self.daemon.desc} on port {self.daemon.bind_port} not running')
 				msg(f'{self.daemon.desc} on port {self.daemon.bind_port} not running')
 			return True
 			return True
 
 
-	def start_daemon(self, silent=False):
+	def start_daemon(self, *, silent=False):
 		return self.daemon.start(silent=silent)
 		return self.daemon.start(silent=silent)
 
 
-	async def restart_daemon(self, quiet=False, silent=False):
+	async def restart_daemon(self, *, quiet=False, silent=False):
 		await self.stop_daemon(quiet=quiet, silent=silent)
 		await self.stop_daemon(quiet=quiet, silent=silent)
 		return self.daemon.start(silent=silent)
 		return self.daemon.start(silent=silent)
 
 
@@ -467,6 +467,7 @@ class RPCClient(MMGenObject):
 async def rpc_init(
 async def rpc_init(
 		cfg,
 		cfg,
 		proto                 = None,
 		proto                 = None,
+		*,
 		backend               = None,
 		backend               = None,
 		daemon                = None,
 		daemon                = None,
 		ignore_daemon_version = False,
 		ignore_daemon_version = False,

+ 2 - 2
mmgen/seed.py

@@ -28,7 +28,7 @@ class SeedID(HiliteStr, InitErrors):
 	color = 'blue'
 	color = 'blue'
 	width = 8
 	width = 8
 	trunc_ok = False
 	trunc_ok = False
-	def __new__(cls, seed=None, sid=None):
+	def __new__(cls, *, seed=None, sid=None):
 		if isinstance(sid, cls):
 		if isinstance(sid, cls):
 			return sid
 			return sid
 		try:
 		try:
@@ -54,7 +54,7 @@ class SeedBase(MMGenObject):
 	data = ImmutableAttr(bytes, typeconv=False)
 	data = ImmutableAttr(bytes, typeconv=False)
 	sid  = ImmutableAttr(SeedID, typeconv=False)
 	sid  = ImmutableAttr(SeedID, typeconv=False)
 
 
-	def __init__(self, cfg, seed_bin=None, nSubseeds=None):
+	def __init__(self, cfg, *, seed_bin=None, nSubseeds=None):
 
 
 		if not seed_bin:
 		if not seed_bin:
 			from .crypto import Crypto
 			from .crypto import Crypto

+ 13 - 10
mmgen/seedsplit.py

@@ -68,7 +68,7 @@ class SeedShareList(SubSeedList):
 	count  = ImmutableAttr(SeedShareCount)
 	count  = ImmutableAttr(SeedShareCount)
 	id_str = ImmutableAttr(SeedSplitIDString)
 	id_str = ImmutableAttr(SeedSplitIDString)
 
 
-	def __init__(self, parent_seed, count, id_str=None, master_idx=None, debug_last_share=False):
+	def __init__(self, parent_seed, count, *, id_str=None, master_idx=None, debug_last_share=False):
 		self.member_type = SeedShare
 		self.member_type = SeedShare
 		self.parent_seed = parent_seed
 		self.parent_seed = parent_seed
 		self.id_str = id_str or 'default'
 		self.id_str = id_str or 'default'
@@ -106,7 +106,8 @@ class SeedShareList(SubSeedList):
 			if last_share_debug(ls) or ls.sid in self.data['long'] or ls.sid == parent_seed.sid:
 			if last_share_debug(ls) or ls.sid in self.data['long'] or ls.sid == parent_seed.sid:
 				# collision: throw out entire split list and redo with new start nonce
 				# collision: throw out entire split list and redo with new start nonce
 				if parent_seed.cfg.debug_subseed:
 				if parent_seed.cfg.debug_subseed:
-					self._collision_debug_msg(ls.sid, count, nonce, 'nonce_start', debug_last_share)
+					self._collision_debug_msg(
+						ls.sid, count, nonce, nonce_desc='nonce_start', debug_last_share=debug_last_share)
 			else:
 			else:
 				self.data['long'][ls.sid] = (count, nonce)
 				self.data['long'][ls.sid] = (count, nonce)
 				break
 				break
@@ -118,7 +119,7 @@ class SeedShareList(SubSeedList):
 			B = self.join().data
 			B = self.join().data
 			assert A == B, f'Data mismatch!\noriginal seed: {A!r}\nrejoined seed: {B!r}'
 			assert A == B, f'Data mismatch!\noriginal seed: {A!r}\nrejoined seed: {B!r}'
 
 
-	def get_share_by_idx(self, idx, base_seed=False):
+	def get_share_by_idx(self, idx, *, base_seed=False):
 		if idx < 1 or idx > self.count:
 		if idx < 1 or idx > self.count:
 			die('RangeError', f'{idx}: share index out of range')
 			die('RangeError', f'{idx}: share index out of range')
 		elif idx == self.count:
 		elif idx == self.count:
@@ -129,7 +130,7 @@ class SeedShareList(SubSeedList):
 			ss_idx = SubSeedIdx(str(idx) + 'L')
 			ss_idx = SubSeedIdx(str(idx) + 'L')
 			return self.get_subseed_by_ss_idx(ss_idx)
 			return self.get_subseed_by_ss_idx(ss_idx)
 
 
-	def get_share_by_seed_id(self, sid, base_seed=False):
+	def get_share_by_seed_id(self, sid, *, base_seed=False):
 		if sid == self.data['long'].key(self.count-1):
 		if sid == self.data['long'].key(self.count-1):
 			return self.last_share
 			return self.last_share
 		elif self.master_share and sid == self.data['long'].key(0):
 		elif self.master_share and sid == self.data['long'].key(0):
@@ -181,7 +182,7 @@ class SeedShareBase(MMGenObject):
 	def desc(self):
 	def desc(self):
 		return self.get_desc()
 		return self.get_desc()
 
 
-	def get_desc(self, ui=False):
+	def get_desc(self, *, ui=False):
 		pl = self.parent_list
 		pl = self.parent_list
 		mss = f', with master share #{pl.master_share.idx}' if pl.master_share else ''
 		mss = f', with master share #{pl.master_share.idx}' if pl.master_share else ''
 		if ui:
 		if ui:
@@ -250,12 +251,11 @@ class SeedShareMaster(SeedBase, SeedShareBase):
 		self.parent_list = parent_list
 		self.parent_list = parent_list
 		self.cfg = parent_list.parent_seed.cfg
 		self.cfg = parent_list.parent_seed.cfg
 
 
-		SeedBase.__init__(self, self.cfg, self.make_base_seed_bin())
+		SeedBase.__init__(self, self.cfg, seed_bin=self.make_base_seed_bin())
 
 
 		self.derived_seed = SeedBase(
 		self.derived_seed = SeedBase(
 			self.cfg,
 			self.cfg,
-			self.make_derived_seed_bin(parent_list.id_str, parent_list.count)
-		)
+			seed_bin = self.make_derived_seed_bin(parent_list.id_str, parent_list.count))
 
 
 	@property
 	@property
 	def fn_stem(self):
 	def fn_stem(self):
@@ -274,7 +274,7 @@ class SeedShareMaster(SeedBase, SeedShareBase):
 		scramble_key = id_str.encode() + b':' + count.to_bytes(2, 'big')
 		scramble_key = id_str.encode() + b':' + count.to_bytes(2, 'big')
 		return Crypto(self.cfg).scramble_seed(self.data, scramble_key)[:self.byte_len]
 		return Crypto(self.cfg).scramble_seed(self.data, scramble_key)[:self.byte_len]
 
 
-	def get_desc(self, ui=False):
+	def get_desc(self, *, ui=False):
 		psid = self.parent_list.parent_seed.sid
 		psid = self.parent_list.parent_seed.sid
 		mss = f'master share #{self.idx} of '
 		mss = f'master share #{self.idx} of '
 		return yellow('(' + mss) + psid.hl() + yellow(')') if ui else mss + psid
 		return yellow('(' + mss) + psid.hl() + yellow(')') if ui else mss + psid
@@ -291,11 +291,14 @@ class SeedShareMasterJoining(SeedShareMaster):
 		self.cfg = cfg
 		self.cfg = cfg
 		self.id_str = id_str or 'default'
 		self.id_str = id_str or 'default'
 		self.count = count
 		self.count = count
-		self.derived_seed = SeedBase(cfg, self.make_derived_seed_bin(self.id_str, self.count))
+		self.derived_seed = SeedBase(
+			cfg,
+			seed_bin = self.make_derived_seed_bin(self.id_str, self.count))
 
 
 def join_shares(
 def join_shares(
 		cfg,
 		cfg,
 		seed_list,
 		seed_list,
+		*,
 		master_idx = None,
 		master_idx = None,
 		id_str     = None):
 		id_str     = None):
 
 

+ 1 - 1
mmgen/sha2.py

@@ -68,7 +68,7 @@ class Sha2:
 		# First wordBits bits of the fractional parts of the cube roots of the first nRounds primes
 		# First wordBits bits of the fractional parts of the cube roots of the first nRounds primes
 		cls.K = tuple(getFractionalBits(cbrt(n)) for n in primes)
 		cls.K = tuple(getFractionalBits(cbrt(n)) for n in primes)
 
 
-	def __init__(self, message, preprocess=True):
+	def __init__(self, message, *, preprocess=True):
 		'Use preprocess=False for Sha256Compress'
 		'Use preprocess=False for Sha256Compress'
 		assert isinstance(message, (bytes, bytearray, list)), 'message must be of type bytes, bytearray or list'
 		assert isinstance(message, (bytes, bytearray, list)), 'message must be of type bytes, bytearray or list'
 		if not self.K:
 		if not self.K:

+ 5 - 5
mmgen/subseed.py

@@ -86,7 +86,7 @@ class SubSeedList(MMGenObject):
 	debug_last_share_sid_len = 3
 	debug_last_share_sid_len = 3
 	dfl_len = 100
 	dfl_len = 100
 
 
-	def __init__(self, parent_seed, length=None):
+	def __init__(self, parent_seed, *, length=None):
 		self.member_type = SubSeed
 		self.member_type = SubSeed
 		self.parent_seed = parent_seed
 		self.parent_seed = parent_seed
 		self.data = {'long': IndexedDict(), 'short': IndexedDict()}
 		self.data = {'long': IndexedDict(), 'short': IndexedDict()}
@@ -95,7 +95,7 @@ class SubSeedList(MMGenObject):
 	def __len__(self):
 	def __len__(self):
 		return len(self.data['long'])
 		return len(self.data['long'])
 
 
-	def get_subseed_by_ss_idx(self, ss_idx_in, print_msg=False):
+	def get_subseed_by_ss_idx(self, ss_idx_in, *, print_msg=False):
 		ss_idx = SubSeedIdx(ss_idx_in)
 		ss_idx = SubSeedIdx(ss_idx_in)
 		if print_msg:
 		if print_msg:
 			msg_r('{} {} of {}...'.format(
 			msg_r('{} {} of {}...'.format(
@@ -122,7 +122,7 @@ class SubSeedList(MMGenObject):
 		assert seed.sid == sid, f'{seed.sid} != {sid}: Seed ID mismatch!'
 		assert seed.sid == sid, f'{seed.sid} != {sid}: Seed ID mismatch!'
 		return seed
 		return seed
 
 
-	def get_subseed_by_seed_id(self, sid, last_idx=None, print_msg=False):
+	def get_subseed_by_seed_id(self, sid, *, last_idx=None, print_msg=False):
 
 
 		def get_existing_subseed_by_seed_id(sid):
 		def get_existing_subseed_by_seed_id(sid):
 			for k in ('long', 'short') if self.have_short else ('long',):
 			for k in ('long', 'short') if self.have_short else ('long',):
@@ -157,7 +157,7 @@ class SubSeedList(MMGenObject):
 			do_msg(subseed)
 			do_msg(subseed)
 			return subseed
 			return subseed
 
 
-	def _collision_debug_msg(self, sid, idx, nonce, nonce_desc='nonce', debug_last_share=False):
+	def _collision_debug_msg(self, sid, idx, nonce, *, nonce_desc='nonce', debug_last_share=False):
 		slen = 'short' if sid in self.data['short'] else 'long'
 		slen = 'short' if sid in self.data['short'] else 'long'
 		m1 = f'add_subseed(idx={idx},{slen}):'
 		m1 = f'add_subseed(idx={idx},{slen}):'
 		if sid == self.parent_seed.sid:
 		if sid == self.parent_seed.sid:
@@ -172,7 +172,7 @@ class SubSeedList(MMGenObject):
 			m2 = f'collision with ID {sid} (idx={colliding_idx},{slen}),'
 			m2 = f'collision with ID {sid} (idx={colliding_idx},{slen}),'
 		msg(f'{m1:30} {m2:46} incrementing {nonce_desc} to {nonce+1}')
 		msg(f'{m1:30} {m2:46} incrementing {nonce_desc} to {nonce+1}')
 
 
-	def _generate(self, last_idx=None, last_sid=None):
+	def _generate(self, last_idx=None, *, last_sid=None):
 
 
 		if last_idx is None:
 		if last_idx is None:
 			last_idx = self.len
 			last_idx = self.len

+ 1 - 1
mmgen/swap/proto/thorchain/memo.py

@@ -121,7 +121,7 @@ class Memo:
 
 
 		return ret(proto_name, function, chain, asset, address, limit_int, int(interval), int(quantity))
 		return ret(proto_name, function, chain, asset, address, limit_int, int(interval), int(quantity))
 
 
-	def __init__(self, proto, addr, chain=None, trade_limit=None):
+	def __init__(self, proto, addr, *, chain=None, trade_limit=None):
 		self.proto = proto
 		self.proto = proto
 		self.chain = chain or proto.coin
 		self.chain = chain or proto.coin
 		if trade_limit is None:
 		if trade_limit is None:

+ 2 - 2
mmgen/swap/proto/thorchain/thornode.py

@@ -22,7 +22,7 @@ class ThornodeRPCClient:
 	verify = True
 	verify = True
 	timeout = 5
 	timeout = 5
 
 
-	def __init__(self, tx, proto=None, host=None):
+	def __init__(self, tx, *, proto=None, host=None):
 		self.cfg = tx.cfg
 		self.cfg = tx.cfg
 		if proto:
 		if proto:
 			self.proto = proto
 			self.proto = proto
@@ -38,7 +38,7 @@ class ThornodeRPCClient:
 				'https': f'socks5h://{self.cfg.proxy}'
 				'https': f'socks5h://{self.cfg.proxy}'
 			})
 			})
 
 
-	def get(self, path, timeout=None):
+	def get(self, path, *, timeout=None):
 		return self.session.get(
 		return self.session.get(
 			url     = self.proto + '://' + self.host + path,
 			url     = self.proto + '://' + self.host + path,
 			timeout = timeout or self.timeout,
 			timeout = timeout or self.timeout,

+ 8 - 8
mmgen/term.py

@@ -50,7 +50,7 @@ class MMGenTerm:
 		pass
 		pass
 
 
 	@classmethod
 	@classmethod
-	def init(cls, noecho=False):
+	def init(cls, *, noecho=False):
 		pass
 		pass
 
 
 	@classmethod
 	@classmethod
@@ -93,7 +93,7 @@ class MMGenTermLinux(MMGenTerm):
 		cls.cur_term = termios.tcgetattr(cls.stdin_fd)
 		cls.cur_term = termios.tcgetattr(cls.stdin_fd)
 
 
 	@classmethod
 	@classmethod
-	def init(cls, noecho=False):
+	def init(cls, *, noecho=False):
 		cls.stdin_fd = sys.stdin.fileno()
 		cls.stdin_fd = sys.stdin.fileno()
 		cls.cur_term = termios.tcgetattr(cls.stdin_fd)
 		cls.cur_term = termios.tcgetattr(cls.stdin_fd)
 		if not hasattr(cls, 'orig_term'):
 		if not hasattr(cls, 'orig_term'):
@@ -128,7 +128,7 @@ class MMGenTermLinux(MMGenTerm):
 				break
 				break
 
 
 	@classmethod
 	@classmethod
-	def get_char(cls, prompt='', immed_chars='', prehold_protect=True, num_bytes=5):
+	def get_char(cls, prompt='', *, immed_chars='', prehold_protect=True, num_bytes=5):
 		"""
 		"""
 		Use os.read(), not file.read(), to get a variable number of bytes without blocking.
 		Use os.read(), not file.read(), to get a variable number of bytes without blocking.
 		Request 5 bytes to cover escape sequences generated by F1, F2, .. Fn keys (5 bytes)
 		Request 5 bytes to cover escape sequences generated by F1, F2, .. Fn keys (5 bytes)
@@ -169,7 +169,7 @@ class MMGenTermLinuxStub(MMGenTermLinux):
 		pass
 		pass
 
 
 	@classmethod
 	@classmethod
-	def init(cls, noecho=False):
+	def init(cls, *, noecho=False):
 		cls.stdin_fd = sys.stdin.fileno()
 		cls.stdin_fd = sys.stdin.fileno()
 
 
 	@classmethod
 	@classmethod
@@ -181,7 +181,7 @@ class MMGenTermLinuxStub(MMGenTermLinux):
 		pass
 		pass
 
 
 	@classmethod
 	@classmethod
-	def get_char(cls, prompt='', immed_chars='', prehold_protect=None, num_bytes=5):
+	def get_char(cls, prompt='', *, immed_chars='', prehold_protect=None, num_bytes=5):
 		msg_r(prompt)
 		msg_r(prompt)
 		return os.read(0, num_bytes).decode()
 		return os.read(0, num_bytes).decode()
 
 
@@ -230,7 +230,7 @@ class MMGenTermMSWin(MMGenTerm):
 					return
 					return
 
 
 	@classmethod
 	@classmethod
-	def get_char(cls, prompt='', immed_chars='', prehold_protect=True, num_bytes=None):
+	def get_char(cls, prompt='', *, immed_chars='', prehold_protect=True, num_bytes=None):
 		"""
 		"""
 		always return a single character, ignore num_bytes
 		always return a single character, ignore num_bytes
 		first character of 2-character sequence returned by F1-F12 keys is discarded
 		first character of 2-character sequence returned by F1-F12 keys is discarded
@@ -268,7 +268,7 @@ class MMGenTermMSWin(MMGenTerm):
 class MMGenTermMSWinStub(MMGenTermMSWin):
 class MMGenTermMSWinStub(MMGenTermMSWin):
 
 
 	@classmethod
 	@classmethod
-	def get_char(cls, prompt='', immed_chars='', prehold_protect=None, num_bytes=None):
+	def get_char(cls, prompt='', *, immed_chars='', prehold_protect=None, num_bytes=None):
 		"""
 		"""
 		Use stdin to allow UTF-8 and emulate the one-character behavior of MMGenTermMSWin
 		Use stdin to allow UTF-8 and emulate the one-character behavior of MMGenTermMSWin
 		"""
 		"""
@@ -289,7 +289,7 @@ def get_term():
 		'win32': (MMGenTermMSWin if sys.stdin.isatty() else MMGenTermMSWinStub),
 		'win32': (MMGenTermMSWin if sys.stdin.isatty() else MMGenTermMSWinStub),
 	}[sys.platform]
 	}[sys.platform]
 
 
-def init_term(cfg, noecho=False):
+def init_term(cfg, *, noecho=False):
 
 
 	term = get_term()
 	term = get_term()
 
 

+ 1 - 1
mmgen/tool/coin.py

@@ -113,7 +113,7 @@ class tool_cmd(tool_cmd_base):
 			gd.ag.to_segwit_redeem_script(data),
 			gd.ag.to_segwit_redeem_script(data),
 			gd.ag.to_addr(data))
 			gd.ag.to_addr(data))
 
 
-	def _privhex2out(self, privhex: 'sstr', output_pubhex=False):
+	def _privhex2out(self, privhex: 'sstr', *, output_pubhex=False):
 		gd = self._init_generators()
 		gd = self._init_generators()
 		pk = PrivKey(
 		pk = PrivKey(
 			self.proto,
 			self.proto,

+ 1 - 1
mmgen/tool/common.py

@@ -31,7 +31,7 @@ class tool_cmd_base(MMGenObject):
 	need_addrtype = False
 	need_addrtype = False
 	need_amt = False
 	need_amt = False
 
 
-	def __init__(self, cfg, cmdname=None, proto=None, mmtype=None):
+	def __init__(self, cfg, *, cmdname=None, proto=None, mmtype=None):
 
 
 		self.cfg = cfg
 		self.cfg = cfg
 
 

+ 2 - 2
mmgen/tool/file.py

@@ -27,7 +27,7 @@ class tool_cmd(tool_cmd_base):
 
 
 	need_proto = True
 	need_proto = True
 
 
-	def __init__(self, cfg, cmdname=None, proto=None, mmtype=None):
+	def __init__(self, cfg, *, cmdname=None, proto=None, mmtype=None):
 		if cmdname == 'txview':
 		if cmdname == 'txview':
 			self.need_amt = True
 			self.need_amt = True
 		super().__init__(cfg=cfg, cmdname=cmdname, proto=proto, mmtype=mmtype)
 		super().__init__(cfg=cfg, cmdname=cmdname, proto=proto, mmtype=mmtype)
@@ -36,7 +36,7 @@ class tool_cmd(tool_cmd_base):
 		kwargs = {'skip_chksum_msg':True}
 		kwargs = {'skip_chksum_msg':True}
 		if not obj.__name__ == 'PasswordList':
 		if not obj.__name__ == 'PasswordList':
 			kwargs.update({'key_address_validity_check':False})
 			kwargs.update({'key_address_validity_check':False})
-		ret = obj(self.cfg, self.proto, mmgen_addrfile, **kwargs)
+		ret = obj(self.cfg, self.proto, infile=mmgen_addrfile, **kwargs)
 		if self.cfg.verbose:
 		if self.cfg.verbose:
 			from ..util import msg, capfirst
 			from ..util import msg, capfirst
 			if ret.al_id.mmtype.name == 'password':
 			if ret.al_id.mmtype.name == 'password':

+ 8 - 8
mmgen/tool/filecrypt.py

@@ -35,20 +35,20 @@ class tool_cmd(tool_cmd_base):
 	* Enc: AES256_CTR, 16-byte rand IV, sha256 hash + 32-byte nonce + data
 	* Enc: AES256_CTR, 16-byte rand IV, sha256 hash + 32-byte nonce + data
 	* The encrypted file is indistinguishable from random data
 	* The encrypted file is indistinguishable from random data
 	"""
 	"""
-	def encrypt(self, infile: str, outfile='', hash_preset=''):
+	def encrypt(self, infile: str, *, outfile='', hash_preset=''):
 		"encrypt a file"
 		"encrypt a file"
-		data = get_data_from_file(self.cfg, infile, 'data for encryption', binary=True)
-		enc_d = Crypto(self.cfg).mmgen_encrypt(data, 'data', hash_preset)
+		data = get_data_from_file(self.cfg, infile, desc='data for encryption', binary=True)
+		enc_d = Crypto(self.cfg).mmgen_encrypt(data, desc='data', hash_preset=hash_preset)
 		if not outfile:
 		if not outfile:
 			outfile = f'{os.path.basename(infile)}.{Crypto.mmenc_ext}'
 			outfile = f'{os.path.basename(infile)}.{Crypto.mmenc_ext}'
-		write_data_to_file(self.cfg, outfile, enc_d, 'encrypted data', binary=True)
+		write_data_to_file(self.cfg, outfile, enc_d, desc='encrypted data', binary=True)
 		return True
 		return True
 
 
-	def decrypt(self, infile: str, outfile='', hash_preset=''):
+	def decrypt(self, infile: str, *, outfile='', hash_preset=''):
 		"decrypt a file"
 		"decrypt a file"
-		enc_d = get_data_from_file(self.cfg, infile, 'encrypted data', binary=True)
+		enc_d = get_data_from_file(self.cfg, infile, desc='encrypted data', binary=True)
 		while True:
 		while True:
-			dec_d = Crypto(self.cfg).mmgen_decrypt(enc_d, 'data', hash_preset)
+			dec_d = Crypto(self.cfg).mmgen_decrypt(enc_d, desc='data', hash_preset=hash_preset)
 			if dec_d:
 			if dec_d:
 				break
 				break
 			from ..util import msg
 			from ..util import msg
@@ -59,5 +59,5 @@ class tool_cmd(tool_cmd_base):
 			outfile = remove_extension(o, Crypto.mmenc_ext)
 			outfile = remove_extension(o, Crypto.mmenc_ext)
 			if outfile == o:
 			if outfile == o:
 				outfile += '.dec'
 				outfile += '.dec'
-		write_data_to_file(self.cfg, outfile, dec_d, 'decrypted data', binary=True)
+		write_data_to_file(self.cfg, outfile, dec_d, desc='decrypted data', binary=True)
 		return True
 		return True

+ 5 - 4
mmgen/tool/fileutil.py

@@ -32,6 +32,7 @@ class tool_cmd(tool_cmd_base):
 	def find_incog_data(self,
 	def find_incog_data(self,
 			filename: str,
 			filename: str,
 			incog_id: str,
 			incog_id: str,
+			*,
 			keep_searching: 'continue search after finding data (ID collisions can yield false positives)' = False):
 			keep_searching: 'continue search after finding data (ID collisions can yield false positives)' = False):
 		"Use an Incog ID to find hidden incognito wallet data"
 		"Use an Incog ID to find hidden incognito wallet data"
 
 
@@ -65,7 +66,7 @@ class tool_cmd(tool_cmd_base):
 		os.close(f)
 		os.close(f)
 		return True
 		return True
 
 
-	def rand2file(self, outfile: str, nbytes: str, threads=4, silent=False):
+	def rand2file(self, outfile: str, nbytes: str, *, threads=4, silent=False):
 		"""
 		"""
 		write ‘nbytes’ bytes of random data to specified file (dd-style byte specifiers supported)
 		write ‘nbytes’ bytes of random data to specified file (dd-style byte specifiers supported)
 
 
@@ -148,7 +149,7 @@ class tool_cmd(tool_cmd_base):
 
 
 		return True
 		return True
 
 
-	def decrypt_keystore(self, wallet_file: str, output_hex=False):
+	def decrypt_keystore(self, wallet_file: str, *, output_hex=False):
 		"decrypt the data in a keystore wallet, returning the decrypted data in binary format"
 		"decrypt the data in a keystore wallet, returning the decrypted data in binary format"
 		from ..ui import line_input
 		from ..ui import line_input
 		passwd = line_input(self.cfg, 'Enter passphrase: ', echo=self.cfg.echo_passphrase).strip().encode()
 		passwd = line_input(self.cfg, 'Enter passphrase: ', echo=self.cfg.echo_passphrase).strip().encode()
@@ -159,9 +160,9 @@ class tool_cmd(tool_cmd_base):
 		ret = decrypt_keystore(data[0]['keystore'], passwd)
 		ret = decrypt_keystore(data[0]['keystore'], passwd)
 		return ret.hex() if output_hex else ret
 		return ret.hex() if output_hex else ret
 
 
-	def decrypt_geth_keystore(self, wallet_file: str, check_addr=True):
+	def decrypt_geth_keystore(self, wallet_file: str, *, check_addr=True):
 		"decrypt the private key in a Geth keystore wallet, returning the decrypted key in hex format"
 		"decrypt the private key in a Geth keystore wallet, returning the decrypted key in hex format"
 		from ..ui import line_input
 		from ..ui import line_input
 		passwd = line_input(self.cfg, 'Enter passphrase: ', echo=self.cfg.echo_passphrase).strip().encode()
 		passwd = line_input(self.cfg, 'Enter passphrase: ', echo=self.cfg.echo_passphrase).strip().encode()
 		from ..proto.eth.misc import decrypt_geth_keystore
 		from ..proto.eth.misc import decrypt_geth_keystore
-		return decrypt_geth_keystore(self.cfg, wallet_file, passwd, check_addr).hex()
+		return decrypt_geth_keystore(self.cfg, wallet_file, passwd, check_addr=check_addr).hex()

+ 1 - 1
mmgen/tool/help.py

@@ -183,7 +183,7 @@ def gen_tool_cmd_usage(mod, cmdname):
 		for line in docstr.split('\n')[1:]:
 		for line in docstr.split('\n')[1:]:
 			yield line.lstrip('\t')
 			yield line.lstrip('\t')
 
 
-def usage(cmdname=None, exit_val=1):
+def usage(cmdname=None, *, exit_val=1):
 
 
 	from ..util import Msg, die
 	from ..util import Msg, die
 
 

+ 4 - 2
mmgen/tool/mnemonic.py

@@ -98,14 +98,15 @@ class tool_cmd(tool_cmd_base):
 		if fmt == 'xmrseed':
 		if fmt == 'xmrseed':
 			hexstr = self._xmr_reduce(bytes.fromhex(hexstr)).hex()
 			hexstr = self._xmr_reduce(bytes.fromhex(hexstr)).hex()
 		f = mnemonic_fmts[fmt]
 		f = mnemonic_fmts[fmt]
-		return ' '.join(f.conv_cls(fmt).fromhex(hexstr, f.pad))
+		return ' '.join(f.conv_cls(fmt).fromhex(hexstr, pad=f.pad))
 
 
 	def mn2hex(self, seed_mnemonic: 'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt):
 	def mn2hex(self, seed_mnemonic: 'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt):
 		"convert a mnemonic seed phrase to a hexadecimal string"
 		"convert a mnemonic seed phrase to a hexadecimal string"
 		f = mnemonic_fmts[fmt]
 		f = mnemonic_fmts[fmt]
-		return f.conv_cls(fmt).tohex(seed_mnemonic.split(), f.pad)
+		return f.conv_cls(fmt).tohex(seed_mnemonic.split(), pad=f.pad)
 
 
 	def mn2hex_interactive(self,
 	def mn2hex_interactive(self,
+			*,
 			fmt: mn_opts_disp = dfl_mnemonic_fmt,
 			fmt: mn_opts_disp = dfl_mnemonic_fmt,
 			mn_len: 'length of seed phrase in words' = 24,
 			mn_len: 'length of seed phrase in words' = 24,
 			print_mn: 'print the seed phrase after entry' = False):
 			print_mn: 'print the seed phrase after entry' = False):
@@ -122,6 +123,7 @@ class tool_cmd(tool_cmd_base):
 		return mnemonic_fmts[fmt].conv_cls(fmt).check_wordlist(self.cfg)
 		return mnemonic_fmts[fmt].conv_cls(fmt).check_wordlist(self.cfg)
 
 
 	def mn_printlist(self,
 	def mn_printlist(self,
+			*,
 			fmt: mn_opts_disp = dfl_mnemonic_fmt,
 			fmt: mn_opts_disp = dfl_mnemonic_fmt,
 			enum: 'enumerate the list' = False,
 			enum: 'enumerate the list' = False,
 			pager: 'send output to pager' = False):
 			pager: 'send output to pager' = False):

+ 15 - 8
mmgen/tool/rpc.py

@@ -50,13 +50,14 @@ class tool_cmd(tool_cmd_base):
 			r = await rpc_init(self.cfg, self.proto, ignore_daemon_version=True, ignore_wallet=True)
 			r = await rpc_init(self.cfg, self.proto, ignore_daemon_version=True, ignore_wallet=True)
 		return f'{d.coind_name} version {r.daemon_version} ({r.daemon_version_str})'
 		return f'{d.coind_name} version {r.daemon_version} ({r.daemon_version_str})'
 
 
-	async def getbalance(self,
+	async def getbalance(self, *,
 			minconf: 'minimum number of confirmations' = 1,
 			minconf: 'minimum number of confirmations' = 1,
 			quiet:   'produce quieter output' = False,
 			quiet:   'produce quieter output' = False,
 			pager:   'send output to pager' = False):
 			pager:   'send output to pager' = False):
 		"list confirmed/unconfirmed, spendable/unspendable balances in tracking wallet"
 		"list confirmed/unconfirmed, spendable/unspendable balances in tracking wallet"
 		from ..tw.bal import TwGetBalance
 		from ..tw.bal import TwGetBalance
-		return (await TwGetBalance(self.cfg, self.proto, minconf, quiet)).format(color=self.cfg.color)
+		return (await TwGetBalance(
+			self.cfg, self.proto, minconf=minconf, quiet=quiet)).format(color=self.cfg.color)
 
 
 	async def twops(self,
 	async def twops(self,
 			obj, pager, reverse, detail, sort, age_fmt, interactive,
 			obj, pager, reverse, detail, sort, age_fmt, interactive,
@@ -81,7 +82,7 @@ class tool_cmd(tool_cmd_base):
 
 
 		return ret
 		return ret
 
 
-	async def twview(self,
+	async def twview(self, *,
 			pager:       'send output to pager' = False,
 			pager:       'send output to pager' = False,
 			reverse:     'reverse order of unspent outputs' = False,
 			reverse:     'reverse order of unspent outputs' = False,
 			wide:        'display data in wide tabular format' = False,
 			wide:        'display data in wide tabular format' = False,
@@ -98,7 +99,7 @@ class tool_cmd(tool_cmd_base):
 			obj, pager, reverse, wide, sort, age_fmt, interactive,
 			obj, pager, reverse, wide, sort, age_fmt, interactive,
 			show_mmid = show_mmid)
 			show_mmid = show_mmid)
 
 
-	async def txhist(self,
+	async def txhist(self, *,
 			pager:       'send output to pager' = False,
 			pager:       'send output to pager' = False,
 			reverse:     'reverse order of transactions' = False,
 			reverse:     'reverse order of transactions' = False,
 			detail:      'produce detailed, non-tabular output' = False,
 			detail:      'produce detailed, non-tabular output' = False,
@@ -114,6 +115,7 @@ class tool_cmd(tool_cmd_base):
 
 
 	async def listaddress(self,
 	async def listaddress(self,
 			mmgen_addr: str,
 			mmgen_addr: str,
+			*,
 			wide:         'display data in wide tabular format' = False,
 			wide:         'display data in wide tabular format' = False,
 			minconf:      'minimum number of confirmations' = 1,
 			minconf:      'minimum number of confirmations' = 1,
 			showcoinaddr: 'display coin address in addition to MMGen ID' = True,
 			showcoinaddr: 'display coin address in addition to MMGen ID' = True,
@@ -127,7 +129,7 @@ class tool_cmd(tool_cmd_base):
 			showcoinaddrs = showcoinaddr,
 			showcoinaddrs = showcoinaddr,
 			age_fmt       = age_fmt)
 			age_fmt       = age_fmt)
 
 
-	async def listaddresses(self,
+	async def listaddresses(self, *,
 			pager:        'send output to pager' = False,
 			pager:        'send output to pager' = False,
 			reverse:      'reverse order of unspent outputs' = False,
 			reverse:      'reverse order of unspent outputs' = False,
 			wide:         'display data in wide tabular format' = False,
 			wide:         'display data in wide tabular format' = False,
@@ -188,7 +190,7 @@ class tool_cmd(tool_cmd_base):
 		from ..tw.ctl import TwCtl
 		from ..tw.ctl import TwCtl
 		return await (await TwCtl(self.cfg, self.proto, mode='w')).rescan_address(mmgen_or_coin_addr)
 		return await (await TwCtl(self.cfg, self.proto, mode='w')).rescan_address(mmgen_or_coin_addr)
 
 
-	async def rescan_blockchain(self,
+	async def rescan_blockchain(self, *,
 			start_block: int = None,
 			start_block: int = None,
 			stop_block: int  = None):
 			stop_block: int  = None):
 		"""
 		"""
@@ -204,7 +206,12 @@ class tool_cmd(tool_cmd_base):
 		await (await TwCtl(self.cfg, self.proto, mode='w')).rescan_blockchain(start_block, stop_block)
 		await (await TwCtl(self.cfg, self.proto, mode='w')).rescan_blockchain(start_block, stop_block)
 		return True
 		return True
 
 
-	async def twexport(self, include_amts=True, pretty=False, prune=False, warn_used=False, force=False):
+	async def twexport(self, *,
+			include_amts = True,
+			pretty       = False,
+			prune        = False,
+			warn_used    = False,
+			force        = False):
 		"""
 		"""
 		export a tracking wallet to JSON format
 		export a tracking wallet to JSON format
 
 
@@ -237,7 +244,7 @@ class tool_cmd(tool_cmd_base):
 			force_overwrite = force)
 			force_overwrite = force)
 		return True
 		return True
 
 
-	async def twimport(self, filename: str, ignore_checksum=False, batch=False):
+	async def twimport(self, filename: str, *, ignore_checksum=False, batch=False):
 		"""
 		"""
 		restore a tracking wallet from a JSON dump created by ‘twexport’
 		restore a tracking wallet from a JSON dump created by ‘twexport’
 
 

+ 16 - 12
mmgen/tool/util.py

@@ -57,6 +57,7 @@ class tool_cmd(tool_cmd_base):
 	def to_bytespec(self,
 	def to_bytespec(self,
 			n: int,
 			n: int,
 			dd_style_byte_specifier: str,
 			dd_style_byte_specifier: str,
+			*,
 			fmt:       'width and precision of output' = '0.2',
 			fmt:       'width and precision of output' = '0.2',
 			print_sym: 'print the specifier after the numerical value' = True,
 			print_sym: 'print the specifier after the numerical value' = True,
 			strip:     'strip trailing zeroes' = False,
 			strip:     'strip trailing zeroes' = False,
@@ -113,6 +114,7 @@ class tool_cmd(tool_cmd_base):
 
 
 	def hexdump(self,
 	def hexdump(self,
 			infile: str,
 			infile: str,
+			*,
 			cols:      'number of columns in output' = 8,
 			cols:      'number of columns in output' = 8,
 			line_nums: "format for line numbers (valid choices: 'hex','dec')" = 'hex'):
 			line_nums: "format for line numbers (valid choices: 'hex','dec')" = 'hex'):
 		"create hexdump of data from file (use '-' for stdin)"
 		"create hexdump of data from file (use '-' for stdin)"
@@ -139,6 +141,7 @@ class tool_cmd(tool_cmd_base):
 	# TODO: handle stdin
 	# TODO: handle stdin
 	def hash256(self,
 	def hash256(self,
 			data: str,
 			data: str,
+			*,
 			file_input: 'first arg is the name of a file containing the data' = False,
 			file_input: 'first arg is the name of a file containing the data' = False,
 			hex_input:  'first arg is a hexadecimal string' = False):
 			hex_input:  'first arg is a hexadecimal string' = False):
 		"compute sha256(sha256(data)) (double sha256)"
 		"compute sha256(sha256(data)) (double sha256)"
@@ -172,7 +175,7 @@ class tool_cmd(tool_cmd_base):
 		return make_chksum_8(
 		return make_chksum_8(
 			get_data_from_file(self.cfg, infile, dash=True, quiet=True, binary=True))
 			get_data_from_file(self.cfg, infile, dash=True, quiet=True, binary=True))
 
 
-	def randb58(self,
+	def randb58(self, *,
 			nbytes: 'number of bytes to output' = 32,
 			nbytes: 'number of bytes to output' = 32,
 			pad:    'pad output to this width' = 0):
 			pad:    'pad output to this width' = 0):
 		"generate random data (default: 32 bytes) and convert it to base 58"
 		"generate random data (default: 32 bytes) and convert it to base 58"
@@ -180,24 +183,24 @@ class tool_cmd(tool_cmd_base):
 		from ..crypto import Crypto
 		from ..crypto import Crypto
 		return baseconv('b58').frombytes(Crypto(self.cfg).get_random(nbytes), pad=pad, tostr=True)
 		return baseconv('b58').frombytes(Crypto(self.cfg).get_random(nbytes), pad=pad, tostr=True)
 
 
-	def bytestob58(self, infile: str, pad: 'pad output to this width' = 0):
+	def bytestob58(self, infile: str, *, pad: 'pad output to this width' = 0):
 		"convert bytes to base 58 (supply data via STDIN)"
 		"convert bytes to base 58 (supply data via STDIN)"
 		from ..fileutil import get_data_from_file
 		from ..fileutil import get_data_from_file
 		from ..baseconv import baseconv
 		from ..baseconv import baseconv
 		data = get_data_from_file(self.cfg, infile, dash=True, quiet=True, binary=True)
 		data = get_data_from_file(self.cfg, infile, dash=True, quiet=True, binary=True)
 		return baseconv('b58').frombytes(data, pad=pad, tostr=True)
 		return baseconv('b58').frombytes(data, pad=pad, tostr=True)
 
 
-	def b58tobytes(self, b58_str: 'sstr', pad: 'pad output to this width' = 0):
+	def b58tobytes(self, b58_str: 'sstr', *, pad: 'pad output to this width' = 0):
 		"convert a base 58 string to bytes (warning: outputs binary data)"
 		"convert a base 58 string to bytes (warning: outputs binary data)"
 		from ..baseconv import baseconv
 		from ..baseconv import baseconv
 		return baseconv('b58').tobytes(b58_str, pad=pad)
 		return baseconv('b58').tobytes(b58_str, pad=pad)
 
 
-	def hextob58(self, hexstr: 'sstr', pad: 'pad output to this width' = 0):
+	def hextob58(self, hexstr: 'sstr', *, pad: 'pad output to this width' = 0):
 		"convert a hexadecimal string to base 58"
 		"convert a hexadecimal string to base 58"
 		from ..baseconv import baseconv
 		from ..baseconv import baseconv
 		return baseconv('b58').fromhex(hexstr, pad=pad, tostr=True)
 		return baseconv('b58').fromhex(hexstr, pad=pad, tostr=True)
 
 
-	def b58tohex(self, b58_str: 'sstr', pad: 'pad output to this width' = 0):
+	def b58tohex(self, b58_str: 'sstr', *, pad: 'pad output to this width' = 0):
 		"convert a base 58 string to hexadecimal"
 		"convert a base 58 string to hexadecimal"
 		from ..baseconv import baseconv
 		from ..baseconv import baseconv
 		return baseconv('b58').tohex(b58_str, pad=pad)
 		return baseconv('b58').tohex(b58_str, pad=pad)
@@ -212,28 +215,29 @@ class tool_cmd(tool_cmd_base):
 		from ..proto.btc.common import b58chk_decode
 		from ..proto.btc.common import b58chk_decode
 		return b58chk_decode(b58chk_str).hex()
 		return b58chk_decode(b58chk_str).hex()
 
 
-	def hextob32(self, hexstr: 'sstr', pad: 'pad output to this width' = 0):
+	def hextob32(self, hexstr: 'sstr', *, pad: 'pad output to this width' = 0):
 		"convert a hexadecimal string to an MMGen-flavor base 32 string"
 		"convert a hexadecimal string to an MMGen-flavor base 32 string"
 		from ..baseconv import baseconv
 		from ..baseconv import baseconv
-		return baseconv('b32').fromhex(hexstr, pad, tostr=True)
+		return baseconv('b32').fromhex(hexstr, pad=pad, tostr=True)
 
 
-	def b32tohex(self, b32_str: 'sstr', pad: 'pad output to this width' = 0):
+	def b32tohex(self, b32_str: 'sstr', *, pad: 'pad output to this width' = 0):
 		"convert an MMGen-flavor base 32 string to hexadecimal"
 		"convert an MMGen-flavor base 32 string to hexadecimal"
 		from ..baseconv import baseconv
 		from ..baseconv import baseconv
-		return baseconv('b32').tohex(b32_str.upper(), pad)
+		return baseconv('b32').tohex(b32_str.upper(), pad=pad)
 
 
 	def hextob6d(self,
 	def hextob6d(self,
 			hexstr: 'sstr',
 			hexstr: 'sstr',
+			*,
 			pad: 'pad output to this width' = 0,
 			pad: 'pad output to this width' = 0,
 			add_spaces: 'add a space after every 5th character' = True):
 			add_spaces: 'add a space after every 5th character' = True):
 		"convert a hexadecimal string to die roll base6 (base6d)"
 		"convert a hexadecimal string to die roll base6 (base6d)"
 		from ..baseconv import baseconv
 		from ..baseconv import baseconv
 		from ..util2 import block_format
 		from ..util2 import block_format
-		ret = baseconv('b6d').fromhex(hexstr, pad, tostr=True)
+		ret = baseconv('b6d').fromhex(hexstr, pad=pad, tostr=True)
 		return block_format(ret, gw=5, cols=None).strip() if add_spaces else ret
 		return block_format(ret, gw=5, cols=None).strip() if add_spaces else ret
 
 
-	def b6dtohex(self, b6d_str: 'sstr', pad: 'pad output to this width' = 0):
+	def b6dtohex(self, b6d_str: 'sstr', *, pad: 'pad output to this width' = 0):
 		"convert a die roll base6 (base6d) string to hexadecimal"
 		"convert a die roll base6 (base6d) string to hexadecimal"
 		from ..baseconv import baseconv
 		from ..baseconv import baseconv
 		from ..util import remove_whitespace
 		from ..util import remove_whitespace
-		return baseconv('b6d').tohex(remove_whitespace(b6d_str), pad)
+		return baseconv('b6d').tohex(remove_whitespace(b6d_str), pad=pad)

+ 19 - 16
mmgen/tool/wallet.py

@@ -29,7 +29,7 @@ from ..wallet import Wallet
 class tool_cmd(tool_cmd_base):
 class tool_cmd(tool_cmd_base):
 	"key, address or subseed generation from an MMGen wallet"
 	"key, address or subseed generation from an MMGen wallet"
 
 
-	def __init__(self, cfg, cmdname=None, proto=None, mmtype=None):
+	def __init__(self, cfg, *, cmdname=None, proto=None, mmtype=None):
 		self.need_proto = cmdname in ('gen_key', 'gen_addr')
 		self.need_proto = cmdname in ('gen_key', 'gen_addr')
 		super().__init__(cfg, cmdname=cmdname, proto=proto, mmtype=mmtype)
 		super().__init__(cfg, cmdname=cmdname, proto=proto, mmtype=mmtype)
 
 
@@ -40,49 +40,52 @@ class tool_cmd(tool_cmd_base):
 			wallets = [wallet] if wallet else [],
 			wallets = [wallet] if wallet else [],
 			nargs   = 1)
 			nargs   = 1)
 
 
-	def get_subseed(self, subseed_idx: str, wallet=''):
+	def get_subseed(self, subseed_idx: str, *, wallet=''):
 		"get the Seed ID of a single subseed by Subseed Index for default or specified wallet"
 		"get the Seed ID of a single subseed by Subseed Index for default or specified wallet"
 		self.cfg._set_quiet(True)
 		self.cfg._set_quiet(True)
-		return Wallet(self.cfg, self._get_seed_file(wallet)).seed.subseed(subseed_idx).sid
+		return Wallet(self.cfg, fn=self._get_seed_file(wallet)).seed.subseed(subseed_idx).sid
 
 
-	def get_subseed_by_seed_id(self, seed_id: str, wallet='', last_idx=SubSeedList.dfl_len):
+	def get_subseed_by_seed_id(self, seed_id: str, *, wallet='', last_idx=SubSeedList.dfl_len):
 		"get the Subseed Index of a single subseed by Seed ID for default or specified wallet"
 		"get the Subseed Index of a single subseed by Seed ID for default or specified wallet"
 		self.cfg._set_quiet(True)
 		self.cfg._set_quiet(True)
-		ret = Wallet(self.cfg, self._get_seed_file(wallet)).seed.subseed_by_seed_id(seed_id, last_idx)
+		ret = Wallet(
+			self.cfg,
+			fn = self._get_seed_file(wallet)).seed.subseed_by_seed_id(seed_id, last_idx=last_idx)
 		return ret.ss_idx if ret else None
 		return ret.ss_idx if ret else None
 
 
-	def list_subseeds(self, subseed_idx_range: str, wallet=''):
+	def list_subseeds(self, subseed_idx_range: str, *, wallet=''):
 		"list a range of subseed Seed IDs for default or specified wallet"
 		"list a range of subseed Seed IDs for default or specified wallet"
 		self.cfg._set_quiet(True)
 		self.cfg._set_quiet(True)
 		from ..subseed import SubSeedIdxRange
 		from ..subseed import SubSeedIdxRange
-		return Wallet(self.cfg, self._get_seed_file(wallet)).seed.subseeds.format(
+		return Wallet(self.cfg, fn=self._get_seed_file(wallet)).seed.subseeds.format(
 			*SubSeedIdxRange(subseed_idx_range))
 			*SubSeedIdxRange(subseed_idx_range))
 
 
 	def list_shares(self,
 	def list_shares(self,
 			share_count: int,
 			share_count: int,
+			*,
 			id_str = 'default',
 			id_str = 'default',
 			master_share: f'(min:1, max:{MasterShareIdx.max_val}, 0=no master share)' = 0,
 			master_share: f'(min:1, max:{MasterShareIdx.max_val}, 0=no master share)' = 0,
 			wallet = ''):
 			wallet = ''):
 		"list the Seed IDs of the shares resulting from a split of default or specified wallet"
 		"list the Seed IDs of the shares resulting from a split of default or specified wallet"
 		self.cfg._set_quiet(True)
 		self.cfg._set_quiet(True)
-		return Wallet(self.cfg, self._get_seed_file(wallet)).seed.split(
-			share_count, id_str, master_share).format()
+		return Wallet(self.cfg, fn=self._get_seed_file(wallet)).seed.split(
+			share_count, id_str=id_str, master_idx=master_share).format()
 
 
-	def gen_key(self, mmgen_addr: str, wallet=''):
+	def gen_key(self, mmgen_addr: str, *, wallet=''):
 		"generate a single WIF key for specified MMGen address from default or specified wallet"
 		"generate a single WIF key for specified MMGen address from default or specified wallet"
-		return self._gen_keyaddr(mmgen_addr, 'wif', wallet)
+		return self._gen_keyaddr(mmgen_addr, 'wif', wallet=wallet)
 
 
-	def gen_addr(self, mmgen_addr: str, wallet=''):
+	def gen_addr(self, mmgen_addr: str, *, wallet=''):
 		"generate a single MMGen address from default or specified wallet"
 		"generate a single MMGen address from default or specified wallet"
-		return self._gen_keyaddr(mmgen_addr, 'addr', wallet)
+		return self._gen_keyaddr(mmgen_addr, 'addr', wallet=wallet)
 
 
-	def _gen_keyaddr(self, mmgen_addr, target, wallet=''):
+	def _gen_keyaddr(self, mmgen_addr, target, *, wallet=''):
 		from ..addr import MMGenID
 		from ..addr import MMGenID
 		from ..addrlist import AddrList, AddrIdxList
 		from ..addrlist import AddrList, AddrIdxList
 
 
 		addr = MMGenID(self.proto, mmgen_addr)
 		addr = MMGenID(self.proto, mmgen_addr)
 		self.cfg._set_quiet(True)
 		self.cfg._set_quiet(True)
-		ss = Wallet(self.cfg, self._get_seed_file(wallet))
+		ss = Wallet(self.cfg, fn=self._get_seed_file(wallet))
 
 
 		if ss.seed.sid != addr.sid:
 		if ss.seed.sid != addr.sid:
 			from ..util import die
 			from ..util import die
@@ -92,7 +95,7 @@ class tool_cmd(tool_cmd_base):
 			cfg       = self.cfg,
 			cfg       = self.cfg,
 			proto     = self.proto,
 			proto     = self.proto,
 			seed      = ss.seed,
 			seed      = ss.seed,
-			addr_idxs = AddrIdxList(str(addr.idx)),
+			addr_idxs = AddrIdxList(fmt_str=str(addr.idx)),
 			mmtype    = addr.mmtype,
 			mmtype    = addr.mmtype,
 			skip_chksum = True).data[0]
 			skip_chksum = True).data[0]
 
 

+ 14 - 13
mmgen/tw/addresses.py

@@ -68,7 +68,7 @@ class TwAddresses(TwView):
 	def coinaddr_list(self):
 	def coinaddr_list(self):
 		return [d.addr for d in self.data]
 		return [d.addr for d in self.data]
 
 
-	async def __init__(self, cfg, proto, minconf=1, mmgen_addrs='', get_data=False):
+	async def __init__(self, cfg, proto, *, minconf=1, mmgen_addrs='', get_data=False):
 
 
 		await super().__init__(cfg, proto)
 		await super().__init__(cfg, proto)
 
 
@@ -82,7 +82,7 @@ class TwAddresses(TwView):
 					f'{mmgen_addrs}: invalid address list argument ' +
 					f'{mmgen_addrs}: invalid address list argument ' +
 					'(must be in form <seed ID>:[<type>:]<idx list>)')
 					'(must be in form <seed ID>:[<type>:]<idx list>)')
 			from ..addrlist import AddrIdxList
 			from ..addrlist import AddrIdxList
-			self.usr_addr_list = [MMGenID(self.proto, f'{a[0]}:{i}') for i in AddrIdxList(a[1])]
+			self.usr_addr_list = [MMGenID(self.proto, f'{a[0]}:{i}') for i in AddrIdxList(fmt_str=a[1])]
 		else:
 		else:
 			self.usr_addr_list = []
 			self.usr_addr_list = []
 
 
@@ -174,22 +174,22 @@ class TwAddresses(TwView):
 	def squeezed_format_line(self, n, d, cw, fs, color, yes, no):
 	def squeezed_format_line(self, n, d, cw, fs, color, yes, no):
 		return fs.format(
 		return fs.format(
 			n = str(n) + ')',
 			n = str(n) + ')',
-			m = d.twmmid.fmt(width=cw.mmid, color=color),
+			m = d.twmmid.fmt(cw.mmid, color=color),
 			u = yes if d.recvd else no,
 			u = yes if d.recvd else no,
-			a = d.addr.fmt(self.addr_view_pref, width=cw.addr, color=color),
-			c = d.comment.fmt2(width=cw.comment, color=color, nullrepl='-'),
-			A = d.amt.fmt(color=color, iwidth=cw.iwidth, prec=self.disp_prec),
+			a = d.addr.fmt(self.addr_view_pref, cw.addr, color=color),
+			c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'),
+			A = d.amt.fmt(cw.iwidth, color=color, prec=self.disp_prec),
 			d = self.age_disp(d, self.age_fmt)
 			d = self.age_disp(d, self.age_fmt)
 		)
 		)
 
 
 	def detail_format_line(self, n, d, cw, fs, color, yes, no):
 	def detail_format_line(self, n, d, cw, fs, color, yes, no):
 		return fs.format(
 		return fs.format(
 			n = str(n) + ')',
 			n = str(n) + ')',
-			m = d.twmmid.fmt(width=cw.mmid, color=color),
+			m = d.twmmid.fmt(cw.mmid, color=color),
 			u = yes if d.recvd else no,
 			u = yes if d.recvd else no,
-			a = d.addr.fmt(self.addr_view_pref, width=cw.addr, color=color),
-			c = d.comment.fmt2(width=cw.comment, color=color, nullrepl='-'),
-			A = d.amt.fmt(color=color, iwidth=cw.iwidth, prec=self.disp_prec),
+			a = d.addr.fmt(self.addr_view_pref, cw.addr, color=color),
+			c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'),
+			A = d.amt.fmt(cw.iwidth, color=color, prec=self.disp_prec),
 			b = self.age_disp(d, 'block'),
 			b = self.age_disp(d, 'block'),
 			D = self.age_disp(d, 'date_time'))
 			D = self.age_disp(d, 'date_time'))
 
 
@@ -274,7 +274,7 @@ class TwAddresses(TwView):
 				return bool(e.recvd)
 				return bool(e.recvd)
 		return None # addr not in tracking wallet
 		return None # addr not in tracking wallet
 
 
-	def get_change_address(self, al_id, bot=None, top=None, exclude=None, desc=None):
+	def get_change_address(self, al_id, *, bot=None, top=None, exclude=None, desc=None):
 		"""
 		"""
 		Get lowest-indexed unused address in tracking wallet for requested AddrListID.
 		Get lowest-indexed unused address in tracking wallet for requested AddrListID.
 		Return values on failure:
 		Return values on failure:
@@ -333,7 +333,7 @@ class TwAddresses(TwView):
 					break
 					break
 			return False
 			return False
 
 
-	def get_change_address_by_addrtype(self, mmtype, exclude, desc):
+	def get_change_address_by_addrtype(self, mmtype, *, exclude, desc):
 		"""
 		"""
 		Find the lowest-indexed change addresses in tracking wallet of given address type,
 		Find the lowest-indexed change addresses in tracking wallet of given address type,
 		present them in a menu and return a single change address chosen by the user.
 		present them in a menu and return a single change address chosen by the user.
@@ -366,7 +366,8 @@ class TwAddresses(TwView):
 				msg(f'{res}: invalid entry')
 				msg(f'{res}: invalid entry')
 
 
 		def get_addr(mmtype):
 		def get_addr(mmtype):
-			return [self.get_change_address(f'{sid}:{mmtype}', r.bot, r.top, exclude=exclude, desc=desc)
+			return [self.get_change_address(
+				f'{sid}:{mmtype}', bot=r.bot, top=r.top, exclude=exclude, desc=desc)
 					for sid, r in self.sid_ranges.items()]
 					for sid, r in self.sid_ranges.items()]
 
 
 		assert isinstance(mmtype, (type(None), MMGenAddrType))
 		assert isinstance(mmtype, (type(None), MMGenAddrType))

+ 3 - 3
mmgen/tw/bal.py

@@ -26,10 +26,10 @@ from ..obj import NonNegativeInt
 
 
 class TwGetBalance(MMGenObject, metaclass=AsyncInit):
 class TwGetBalance(MMGenObject, metaclass=AsyncInit):
 
 
-	def __new__(cls, cfg, proto, *args, **kwargs):
+	def __new__(cls, cfg, proto, *, minconf, quiet):
 		return MMGenObject.__new__(proto.base_proto_subclass(cls, 'tw.bal'))
 		return MMGenObject.__new__(proto.base_proto_subclass(cls, 'tw.bal'))
 
 
-	async def __init__(self, cfg, proto, minconf, quiet):
+	async def __init__(self, cfg, proto, *, minconf, quiet):
 
 
 		class BalanceInfo(dict):
 		class BalanceInfo(dict):
 			def __init__(self):
 			def __init__(self):
@@ -65,7 +65,7 @@ class TwGetBalance(MMGenObject, metaclass=AsyncInit):
 					return len(str(int(max(v[colname] for v in self.data.values())))) + iwidth_adj
 					return len(str(int(max(v[colname] for v in self.data.values())))) + iwidth_adj
 
 
 				def make_col(label, col):
 				def make_col(label, col):
-					return self.data[label][col].fmt(iwidth=iwidths[col], color=color)
+					return self.data[label][col].fmt(iwidths[col], color=color)
 
 
 				if color:
 				if color:
 					from ..color import green, yellow
 					from ..color import green, yellow

+ 8 - 6
mmgen/tw/ctl.py

@@ -63,6 +63,7 @@ class TwCtl(MMGenObject, metaclass=AsyncInit):
 			self,
 			self,
 			cfg,
 			cfg,
 			proto,
 			proto,
+			*,
 			mode              = 'r',
 			mode              = 'r',
 			token_addr        = None,
 			token_addr        = None,
 			no_rpc            = False,
 			no_rpc            = False,
@@ -167,7 +168,7 @@ class TwCtl(MMGenObject, metaclass=AsyncInit):
 	def data_root_desc(self):
 	def data_root_desc(self):
 		return self.data_key
 		return self.data_key
 
 
-	def cache_balance(self, addr, bal, session_cache, data_root, force=False):
+	def cache_balance(self, addr, bal, *, session_cache, data_root, force=False):
 		if force or addr not in session_cache:
 		if force or addr not in session_cache:
 			session_cache[addr] = str(bal)
 			session_cache[addr] = str(bal)
 			if addr in data_root:
 			if addr in data_root:
@@ -183,11 +184,11 @@ class TwCtl(MMGenObject, metaclass=AsyncInit):
 		if addr in data_root and 'balance' in data_root[addr]:
 		if addr in data_root and 'balance' in data_root[addr]:
 			return self.proto.coin_amt(data_root[addr]['balance'])
 			return self.proto.coin_amt(data_root[addr]['balance'])
 
 
-	async def get_balance(self, addr, force_rpc=False):
+	async def get_balance(self, addr, *, force_rpc=False):
 		ret = None if force_rpc else self.get_cached_balance(addr, self.cur_balances, self.data_root)
 		ret = None if force_rpc else self.get_cached_balance(addr, self.cur_balances, self.data_root)
 		if ret is None:
 		if ret is None:
 			ret = await self.rpc_get_balance(addr)
 			ret = await self.rpc_get_balance(addr)
-			self.cache_balance(addr, ret, self.cur_balances, self.data_root)
+			self.cache_balance(addr, ret, session_cache=self.cur_balances, data_root=self.data_root)
 		return ret
 		return ret
 
 
 	def force_write(self):
 	def force_write(self):
@@ -212,7 +213,7 @@ class TwCtl(MMGenObject, metaclass=AsyncInit):
 
 
 		self.orig_data = data
 		self.orig_data = data
 
 
-	def write(self, quiet=True):
+	def write(self, *, quiet=True):
 		if not self.use_tw_file:
 		if not self.use_tw_file:
 			self.cfg._util.dmsg("'use_tw_file' is False, doing nothing")
 			self.cfg._util.dmsg("'use_tw_file' is False, doing nothing")
 			return
 			return
@@ -256,6 +257,7 @@ class TwCtl(MMGenObject, metaclass=AsyncInit):
 			self,
 			self,
 			addrspec,
 			addrspec,
 			comment      = '',
 			comment      = '',
+			*,
 			trusted_pair = None,
 			trusted_pair = None,
 			silent       = False):
 			silent       = False):
 
 
@@ -296,11 +298,11 @@ class TwCtl(MMGenObject, metaclass=AsyncInit):
 	async def remove_comment(self, mmaddr):
 	async def remove_comment(self, mmaddr):
 		await self.set_comment(mmaddr, '')
 		await self.set_comment(mmaddr, '')
 
 
-	async def import_address_common(self, data, batch=False, gather=False):
+	async def import_address_common(self, data, *, batch=False, gather=False):
 
 
 		async def do_import(address, comment, message):
 		async def do_import(address, comment, message):
 			try:
 			try:
-				res = await self.import_address(address, comment)
+				res = await self.import_address(address, label=comment)
 				self.cfg._util.qmsg(message)
 				self.cfg._util.qmsg(message)
 				return res
 				return res
 			except Exception as e:
 			except Exception as e:

+ 5 - 2
mmgen/tw/json.py

@@ -30,7 +30,8 @@ class TwJSON:
 		fn_pfx = 'mmgen-tracking-wallet-dump'
 		fn_pfx = 'mmgen-tracking-wallet-dump'
 
 
 		def __new__(cls, cfg, proto, *args, **kwargs):
 		def __new__(cls, cfg, proto, *args, **kwargs):
-			return MMGenObject.__new__(proto.base_proto_subclass(TwJSON, 'tw.json', cls.__name__))
+			return MMGenObject.__new__(
+				proto.base_proto_subclass(TwJSON, 'tw.json', sub_clsname=cls.__name__))
 
 
 		def __init__(self, cfg, proto):
 		def __init__(self, cfg, proto):
 			self.cfg = cfg
 			self.cfg = cfg
@@ -62,7 +63,7 @@ class TwJSON:
 
 
 			return fn
 			return fn
 
 
-		def json_dump(self, data, pretty=False):
+		def json_dump(self, data, *, pretty=False):
 			return json.dumps(
 			return json.dumps(
 				data,
 				data,
 				cls        = json_encoder,
 				cls        = json_encoder,
@@ -90,6 +91,7 @@ class TwJSON:
 				cfg,
 				cfg,
 				proto,
 				proto,
 				filename,
 				filename,
+				*,
 				ignore_checksum = False,
 				ignore_checksum = False,
 				batch           = False):
 				batch           = False):
 
 
@@ -163,6 +165,7 @@ class TwJSON:
 				self,
 				self,
 				cfg,
 				cfg,
 				proto,
 				proto,
+				*,
 				include_amts    = True,
 				include_amts    = True,
 				pretty          = False,
 				pretty          = False,
 				prune           = False,
 				prune           = False,

+ 2 - 2
mmgen/tw/shared.py

@@ -45,8 +45,8 @@ class TwMMGenID(HiliteStr, InitErrors, MMGenObject):
 		me.proto = proto
 		me.proto = proto
 		return me
 		return me
 
 
-	def fmt(self, **kwargs):
-		return super().fmtc(self.disp, **kwargs)
+	def fmt(self, width, /, **kwargs):
+		return super().fmtc(self.disp, width, **kwargs)
 
 
 # non-displaying container for TwMMGenID, TwComment
 # non-displaying container for TwMMGenID, TwComment
 class TwLabel(str, InitErrors, MMGenObject):
 class TwLabel(str, InitErrors, MMGenObject):

+ 4 - 4
mmgen/tw/txhistory.py

@@ -41,7 +41,7 @@ class TwTxHistory(TwView):
 	filters = ('show_unconfirmed',)
 	filters = ('show_unconfirmed',)
 	mod_subpath = 'tw.txhistory'
 	mod_subpath = 'tw.txhistory'
 
 
-	async def __init__(self, cfg, proto, sinceblock=0):
+	async def __init__(self, cfg, proto, *, sinceblock=0):
 		await super().__init__(cfg, proto)
 		await super().__init__(cfg, proto)
 		self.sinceblock = NonNegativeInt(sinceblock if sinceblock >= 0 else self.rpc.blockcount + sinceblock)
 		self.sinceblock = NonNegativeInt(sinceblock if sinceblock >= 0 else self.rpc.blockcount + sinceblock)
 
 
@@ -57,7 +57,7 @@ class TwTxHistory(TwView):
 		amts_tuple = namedtuple('amts_data', ['amt'])
 		amts_tuple = namedtuple('amts_data', ['amt'])
 		return super().set_amt_widths([amts_tuple(d.amt_disp(self.show_total_amt)) for d in data])
 		return super().set_amt_widths([amts_tuple(d.amt_disp(self.show_total_amt)) for d in data])
 
 
-	def get_column_widths(self, data, wide, interactive):
+	def get_column_widths(self, data, *, wide, interactive):
 
 
 		# var cols: inputs outputs comment [txid]
 		# var cols: inputs outputs comment [txid]
 		if not hasattr(self, 'varcol_maxwidths'):
 		if not hasattr(self, 'varcol_maxwidths'):
@@ -132,9 +132,9 @@ class TwTxHistory(TwView):
 				t = d.txid_disp(width=cw.txid, color=color) if hasattr(cw, 'txid') else None,
 				t = d.txid_disp(width=cw.txid, color=color) if hasattr(cw, 'txid') else None,
 				d = d.age_disp(self.age_fmt, width=self.age_w, color=color),
 				d = d.age_disp(self.age_fmt, width=self.age_w, color=color),
 				i = d.vouts_disp('inputs', width=cw.inputs, color=color, addr_view_pref=self.addr_view_pref),
 				i = d.vouts_disp('inputs', width=cw.inputs, color=color, addr_view_pref=self.addr_view_pref),
-				A = d.amt_disp(self.show_total_amt).fmt(iwidth=cw.iwidth, prec=self.disp_prec, color=color),
+				A = d.amt_disp(self.show_total_amt).fmt(cw.iwidth, prec=self.disp_prec, color=color),
 				o = d.vouts_disp('outputs', width=cw.outputs, color=color, addr_view_pref=self.addr_view_pref),
 				o = d.vouts_disp('outputs', width=cw.outputs, color=color, addr_view_pref=self.addr_view_pref),
-				c = d.comment.fmt2(width=cw.comment, color=color, nullrepl='-'))
+				c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'))
 
 
 	def gen_detail_display(self, data, cw, fs, color, fmt_method):
 	def gen_detail_display(self, data, cw, fs, color, fmt_method):
 
 

+ 20 - 19
mmgen/tw/unspent.py

@@ -70,7 +70,7 @@ class TwUnspentOutputs(TwView):
 			self.__dict__['proto'] = proto
 			self.__dict__['proto'] = proto
 			MMGenListItem.__init__(self, **kwargs)
 			MMGenListItem.__init__(self, **kwargs)
 
 
-	async def __init__(self, cfg, proto, minconf=1, addrs=[]):
+	async def __init__(self, cfg, proto, *, minconf=1, addrs=[]):
 		await super().__init__(cfg, proto)
 		await super().__init__(cfg, proto)
 		self.minconf  = minconf
 		self.minconf  = minconf
 		self.addrs    = addrs
 		self.addrs    = addrs
@@ -177,16 +177,16 @@ class TwUnspentOutputs(TwView):
 		for n, d in enumerate(data):
 		for n, d in enumerate(data):
 			yield fs.format(
 			yield fs.format(
 				n = str(n+1) + ')',
 				n = str(n+1) + ')',
-				t = (d.txid.fmtc('|' + '.'*(cw.txid-1), width=cw.txid, color=color) if d.skip  == 'txid'
-					else d.txid.truncate(width=cw.txid, color=color)) if cw.txid else None,
-				v = ' ' + d.vout.fmt(width=cw.vout-1, color=color) if cw.vout else None,
-				a = d.addr.fmtc('|' + '.'*(cw.addr-1), width=cw.addr, color=color) if d.skip == 'addr'
-					else d.addr.fmt(self.addr_view_pref, width=cw.addr, color=color),
-				m = (d.twmmid.fmtc('.'*cw.mmid, width=cw.mmid, color=color) if d.skip == 'addr'
-					else d.twmmid.fmt(width=cw.mmid, color=color)) if cw.mmid else None,
-				c = d.comment.fmt2(width=cw.comment, color=color, nullrepl='-') if cw.comment else None,
-				A = d.amt.fmt(color=color, iwidth=cw.iwidth, prec=self.disp_prec),
-				B = d.amt2.fmt(color=color, iwidth=cw.iwidth2, prec=self.disp_prec) if cw.amt2 else None,
+				t = (d.txid.fmtc('|' + '.'*(cw.txid-1), cw.txid, color=color) if d.skip  == 'txid'
+					else d.txid.truncate(cw.txid, color=color)) if cw.txid else None,
+				v = ' ' + d.vout.fmt(cw.vout-1, color=color) if cw.vout else None,
+				a = d.addr.fmtc('|' + '.'*(cw.addr-1), cw.addr, color=color) if d.skip == 'addr'
+					else d.addr.fmt(self.addr_view_pref, cw.addr, color=color),
+				m = (d.twmmid.fmtc('.'*cw.mmid, cw.mmid, color=color) if d.skip == 'addr'
+					else d.twmmid.fmt(cw.mmid, color=color)) if cw.mmid else None,
+				c = d.comment.fmt2(cw.comment, color=color, nullrepl='-') if cw.comment else None,
+				A = d.amt.fmt(cw.iwidth, color=color, prec=self.disp_prec),
+				B = d.amt2.fmt(cw.iwidth2, color=color, prec=self.disp_prec) if cw.amt2 else None,
 				d = self.age_disp(d, self.age_fmt),
 				d = self.age_disp(d, self.age_fmt),
 			)
 			)
 
 
@@ -195,21 +195,22 @@ class TwUnspentOutputs(TwView):
 		for n, d in enumerate(data):
 		for n, d in enumerate(data):
 			yield fs.format(
 			yield fs.format(
 				n = str(n+1) + ')',
 				n = str(n+1) + ')',
-				t = d.txid.fmt(width=cw.txid, color=color) if cw.txid else None,
-				v = ' ' + d.vout.fmt(width=cw.vout-1, color=color) if cw.vout else None,
-				a = d.addr.fmt(self.addr_view_pref, width=cw.addr, color=color),
-				m = d.twmmid.fmt(width=cw.mmid, color=color),
-				A = d.amt.fmt(color=color, iwidth=cw.iwidth, prec=self.disp_prec),
-				B = d.amt2.fmt(color=color, iwidth=cw.iwidth2, prec=self.disp_prec) if cw.amt2 else None,
+				t = d.txid.fmt(cw.txid, color=color) if cw.txid else None,
+				v = ' ' + d.vout.fmt(cw.vout-1, color=color) if cw.vout else None,
+				a = d.addr.fmt(self.addr_view_pref, cw.addr, color=color),
+				m = d.twmmid.fmt(cw.mmid, color=color),
+				A = d.amt.fmt(cw.iwidth, color=color, prec=self.disp_prec),
+				B = d.amt2.fmt(cw.iwidth2, color=color, prec=self.disp_prec) if cw.amt2 else None,
 				b = self.age_disp(d, 'block'),
 				b = self.age_disp(d, 'block'),
 				D = self.age_disp(d, 'date_time'),
 				D = self.age_disp(d, 'date_time'),
-				c = d.comment.fmt2(width=cw.comment, color=color, nullrepl='-'))
+				c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'))
 
 
 	def display_total(self):
 	def display_total(self):
-		msg('\nTotal unspent: {} {} ({} output{})'.format(
+		msg('\nTotal unspent: {} {} ({} {}{})'.format(
 			self.total.hl(),
 			self.total.hl(),
 			self.proto.dcoin,
 			self.proto.dcoin,
 			len(self.data),
 			len(self.data),
+			self.item_desc,
 			suf(self.data)))
 			suf(self.data)))
 
 
 	async def set_dates(self, us):
 	async def set_dates(self, us):

+ 6 - 5
mmgen/tw/view.py

@@ -250,14 +250,14 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 		'twmmid': lambda i: '{} {:010} {:024.12f}'.format(i.twmmid.sort_key, 0xffffffff - abs(i.confs), i.amt)
 		'twmmid': lambda i: '{} {:010} {:024.12f}'.format(i.twmmid.sort_key, 0xffffffff - abs(i.confs), i.amt)
 	}
 	}
 
 
-	def sort_info(self, include_group=True):
+	def sort_info(self, *, include_group=True):
 		ret = ([], ['Reverse'])[self.reverse]
 		ret = ([], ['Reverse'])[self.reverse]
 		ret.append(self.sort_disp[self.sort_key])
 		ret.append(self.sort_disp[self.sort_key])
 		if include_group and self.group and (self.sort_key in ('addr', 'txid', 'twmmid')):
 		if include_group and self.group and (self.sort_key in ('addr', 'txid', 'twmmid')):
 			ret.append('Grouped')
 			ret.append('Grouped')
 		return ret
 		return ret
 
 
-	def do_sort(self, key=None, reverse=False):
+	def do_sort(self, key=None, *, reverse=False):
 		key = key or self.sort_key
 		key = key or self.sort_key
 		if key not in self.sort_funcs:
 		if key not in self.sort_funcs:
 			die(1, f'{key!r}: invalid sort key.  Valid options: {" ".join(self.sort_funcs)}')
 			die(1, f'{key!r}: invalid sort key.  Valid options: {" ".join(self.sort_funcs)}')
@@ -268,7 +268,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 		if self.data != save:
 		if self.data != save:
 			self.pos = 0
 			self.pos = 0
 
 
-	async def get_data(self, sort_key=None, reverse_sort=False):
+	async def get_data(self, *, sort_key=None, reverse_sort=False):
 
 
 		rpc_data = await self.get_rpc_data()
 		rpc_data = await self.get_rpc_data()
 
 
@@ -290,7 +290,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 		# so add NL here (' ' required because CUR_HOME erases preceding blank lines)
 		# so add NL here (' ' required because CUR_HOME erases preceding blank lines)
 		msg(' ')
 		msg(' ')
 
 
-	def get_term_dimensions(self, min_cols, min_lines=None):
+	def get_term_dimensions(self, min_cols, *, min_lines=None):
 		from ..term import get_terminal_size, get_char_raw, _term_dimensions
 		from ..term import get_terminal_size, get_char_raw, _term_dimensions
 		user_resized = False
 		user_resized = False
 		while True:
 		while True:
@@ -311,7 +311,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 			else:
 			else:
 				return _term_dimensions(min_cols, ts.height)
 				return _term_dimensions(min_cols, ts.height)
 
 
-	def compute_column_widths(self, widths, maxws, minws, maxws_nice, wide, interactive):
+	def compute_column_widths(self, widths, maxws, minws, maxws_nice, *, wide, interactive):
 
 
 		def do_ret(freews):
 		def do_ret(freews):
 			widths.update({k:minws[k] + freews.get(k, 0) for k in minws})
 			widths.update({k:minws[k] + freews.get(k, 0) for k in minws})
@@ -390,6 +390,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 	async def format(
 	async def format(
 			self,
 			self,
 			display_type,
 			display_type,
+			*,
 			color           = True,
 			color           = True,
 			interactive     = False,
 			interactive     = False,
 			line_processing = None,
 			line_processing = None,

+ 5 - 4
mmgen/tx/base.py

@@ -142,7 +142,7 @@ class Base(MMGenObject):
 	def sum_inputs(self):
 	def sum_inputs(self):
 		return sum(e.amt for e in self.inputs)
 		return sum(e.amt for e in self.inputs)
 
 
-	def sum_outputs(self, exclude=None):
+	def sum_outputs(self, *, exclude=None):
 		if exclude is None:
 		if exclude is None:
 			olist = self.outputs
 			olist = self.outputs
 		else:
 		else:
@@ -178,10 +178,11 @@ class Base(MMGenObject):
 		self.blockcount = self.rpc.blockcount
 		self.blockcount = self.rpc.blockcount
 
 
 	# returns True if comment added or changed, False otherwise
 	# returns True if comment added or changed, False otherwise
-	def add_comment(self, infile=None):
+	def add_comment(self, *, infile=None):
 		if infile:
 		if infile:
 			from ..fileutil import get_data_from_file
 			from ..fileutil import get_data_from_file
-			self.comment = MMGenTxComment(get_data_from_file(self.cfg, infile, 'transaction comment'))
+			self.comment = MMGenTxComment(
+				get_data_from_file(self.cfg, infile, desc='transaction comment'))
 		else:
 		else:
 			from ..ui import keypress_confirm, line_input
 			from ..ui import keypress_confirm, line_input
 			if keypress_confirm(
 			if keypress_confirm(
@@ -203,7 +204,7 @@ class Base(MMGenObject):
 			edesc = 'non-MMGen address',
 			edesc = 'non-MMGen address',
 			quiet = True)
 			quiet = True)
 
 
-	def check_non_mmgen_inputs(self, caller, non_mmaddrs=None):
+	def check_non_mmgen_inputs(self, caller, *, non_mmaddrs=None):
 		non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs')
 		non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs')
 		if non_mmaddrs:
 		if non_mmaddrs:
 			indent = '  '
 			indent = '  '

+ 9 - 9
mmgen/tx/file.py

@@ -45,7 +45,7 @@ def get_proto_from_coin_id(tx, coin_id, chain):
 
 
 	return init_proto(tx.cfg, coin, network=network, need_amt=True, tokensym=tokensym)
 	return init_proto(tx.cfg, coin, network=network, need_amt=True, tokensym=tokensym)
 
 
-def eval_io_data(tx, data, desc):
+def eval_io_data(tx, data, *, desc):
 	if not (desc == 'outputs' and tx.proto.base_coin == 'ETH'): # ETH txs can have no outputs
 	if not (desc == 'outputs' and tx.proto.base_coin == 'ETH'): # ETH txs can have no outputs
 		assert len(data), f'no {desc}!'
 		assert len(data), f'no {desc}!'
 	for d in data:
 	for d in data:
@@ -80,10 +80,10 @@ class MMGenTxFile(MMGenObject):
 		self.fmt_data = None
 		self.fmt_data = None
 		self.filename = None
 		self.filename = None
 
 
-	def parse(self, infile, metadata_only=False, quiet_open=False):
+	def parse(self, infile, *, metadata_only=False, quiet_open=False):
 		tx = self.tx
 		tx = self.tx
 		from ..fileutil import get_data_from_file
 		from ..fileutil import get_data_from_file
-		data = get_data_from_file(tx.cfg, infile, f'{tx.desc} data', quiet=quiet_open)
+		data = get_data_from_file(tx.cfg, infile, desc=f'{tx.desc} data', quiet=quiet_open)
 		if len(data) > tx.cfg.max_tx_file_size:
 		if len(data) > tx.cfg.max_tx_file_size:
 			die('MaxFileSizeExceeded',
 			die('MaxFileSizeExceeded',
 				f'Transaction file size exceeds limit ({tx.cfg.max_tx_file_size} bytes)')
 				f'Transaction file size exceeds limit ({tx.cfg.max_tx_file_size} bytes)')
@@ -112,7 +112,7 @@ class MMGenTxFile(MMGenObject):
 				setattr(tx, k, v(data[k]) if v else data[k])
 				setattr(tx, k, v(data[k]) if v else data[k])
 
 
 		for k in ('inputs', 'outputs'):
 		for k in ('inputs', 'outputs'):
-			setattr(tx, k, eval_io_data(tx, data[k], k))
+			setattr(tx, k, eval_io_data(tx, data[k], desc=k))
 
 
 		tx.check_txfile_hex_data()
 		tx.check_txfile_hex_data()
 
 
@@ -124,7 +124,7 @@ class MMGenTxFile(MMGenObject):
 		tx = self.tx
 		tx = self.tx
 		tx.file_format = 'legacy'
 		tx.file_format = 'legacy'
 
 
-		def deserialize(raw_data, desc):
+		def deserialize(raw_data, *, desc):
 			from ast import literal_eval
 			from ast import literal_eval
 			try:
 			try:
 				return literal_eval(raw_data)
 				return literal_eval(raw_data)
@@ -199,12 +199,12 @@ class MMGenTxFile(MMGenObject):
 			tx.parse_txfile_serialized_data()
 			tx.parse_txfile_serialized_data()
 			for k in ('inputs', 'outputs'):
 			for k in ('inputs', 'outputs'):
 				desc = f'{k} data'
 				desc = f'{k} data'
-				res = deserialize(io_data[k], k)
+				res = deserialize(io_data[k], desc=k)
 				for d in res:
 				for d in res:
 					if 'label' in d:
 					if 'label' in d:
 						d['comment'] = d['label']
 						d['comment'] = d['label']
 						del d['label']
 						del d['label']
-				setattr(tx, k, eval_io_data(tx, res, k))
+				setattr(tx, k, eval_io_data(tx, res, desc=k))
 			desc = 'send amount in metadata'
 			desc = 'send amount in metadata'
 			assert tx.proto.coin_amt(send_amt) == tx.send_amt, f'{send_amt} != {tx.send_amt}'
 			assert tx.proto.coin_amt(send_amt) == tx.send_amt, f'{send_amt} != {tx.send_amt}'
 		except Exception as e:
 		except Exception as e:
@@ -286,7 +286,7 @@ class MMGenTxFile(MMGenObject):
 
 
 		return fmt_data
 		return fmt_data
 
 
-	def write(self,
+	def write(self, *,
 		add_desc              = '',
 		add_desc              = '',
 		outdir                = None,
 		outdir                = None,
 		ask_write             = True,
 		ask_write             = True,
@@ -316,7 +316,7 @@ class MMGenTxFile(MMGenObject):
 			ignore_opt_outdir     = outdir)
 			ignore_opt_outdir     = outdir)
 
 
 	@classmethod
 	@classmethod
-	def get_proto(cls, cfg, filename, quiet_open=False):
+	def get_proto(cls, cfg, filename, *, quiet_open=False):
 		from . import BaseTX
 		from . import BaseTX
 		tmp_tx = BaseTX(cfg=cfg)
 		tmp_tx = BaseTX(cfg=cfg)
 		cls(tmp_tx).parse(filename, metadata_only=True, quiet_open=quiet_open)
 		cls(tmp_tx).parse(filename, metadata_only=True, quiet_open=quiet_open)

+ 8 - 8
mmgen/tx/info.py

@@ -25,7 +25,7 @@ class TxInfo:
 		self.cfg = cfg
 		self.cfg = cfg
 		self.tx = tx
 		self.tx = tx
 
 
-	def format(self, terse=False, sort='addr'):
+	def format(self, *, terse=False, sort='addr'):
 
 
 		tx = self.tx
 		tx = self.tx
 
 
@@ -101,11 +101,11 @@ class TxInfo:
 			iwidth = len(str(int(tx.sum_inputs())))
 			iwidth = len(str(int(tx.sum_inputs())))
 
 
 			yield self.txinfo_ftr_fs.format(
 			yield self.txinfo_ftr_fs.format(
-				i = tx.sum_inputs().fmt(color=True, iwidth=iwidth),
-				o = tx.sum_outputs().fmt(color=True, iwidth=iwidth),
-				C = tx.change.fmt(color=True, iwidth=iwidth),
-				s = tx.send_amt.fmt(color=True, iwidth=iwidth),
-				a = self.format_abs_fee(color=True, iwidth=iwidth),
+				i = tx.sum_inputs().fmt(iwidth, color=True),
+				o = tx.sum_outputs().fmt(iwidth, color=True),
+				C = tx.change.fmt(iwidth, color=True),
+				s = tx.send_amt.fmt(iwidth, color=True),
+				a = self.format_abs_fee(iwidth, color=True),
 				r = self.format_rel_fee(),
 				r = self.format_rel_fee(),
 				d = tx.dcoin,
 				d = tx.dcoin,
 				c = tx.coin)
 				c = tx.coin)
@@ -115,7 +115,7 @@ class TxInfo:
 
 
 		return ''.join(gen_view())
 		return ''.join(gen_view())
 
 
-	def view_with_prompt(self, prompt, pause=True):
+	def view_with_prompt(self, prompt, *, pause=True):
 		prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view: '
 		prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view: '
 		from ..term import get_char
 		from ..term import get_char
 		while True:
 		while True:
@@ -131,7 +131,7 @@ class TxInfo:
 				break
 				break
 			msg('Invalid reply')
 			msg('Invalid reply')
 
 
-	def view(self, pager=False, pause=True, terse=False):
+	def view(self, *, pager=False, pause=True, terse=False):
 		o = self.format(terse=terse)
 		o = self.format(terse=terse)
 		if pager:
 		if pager:
 			from ..ui import do_pager
 			from ..ui import do_pager

+ 25 - 19
mmgen/tx/new.py

@@ -70,6 +70,13 @@ def mmaddr2coinaddr(cfg, mmaddr, ad_w, ad_f, proto):
 
 
 	return CoinAddr(proto, coin_addr)
 	return CoinAddr(proto, coin_addr)
 
 
+def parse_fee_spec(proto, fee_arg):
+	import re
+	units = {u[0]:u for u in proto.coin_amt.units}
+	pat = re.compile(r'((?:[1-9][0-9]*)|(?:[0-9]+\.[0-9]+))({})'.format('|'.join(units)))
+	if m := pat.match(fee_arg):
+		return namedtuple('parsed_fee_spec', ['amt', 'unit'])(m[1], units[m[2]])
+
 class New(Base):
 class New(Base):
 
 
 	fee_is_approximate = False
 	fee_is_approximate = False
@@ -78,7 +85,6 @@ class New(Base):
 		ERROR: No change address specified.  If you wish to create a transaction with
 		ERROR: No change address specified.  If you wish to create a transaction with
 		only one output, specify a single output address with no {} amount
 		only one output, specify a single output address with no {} amount
 	"""
 	"""
-	msg_insufficient_funds = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
 	chg_autoselected = False
 	chg_autoselected = False
 	_funds_available = namedtuple('funds_available', ['is_positive', 'amt'])
 	_funds_available = namedtuple('funds_available', ['is_positive', 'amt'])
 
 
@@ -117,16 +123,12 @@ class New(Base):
 		if fee := get_obj(self.proto.coin_amt, num=fee_arg, silent=True):
 		if fee := get_obj(self.proto.coin_amt, num=fee_arg, silent=True):
 			return fee
 			return fee
 
 
-		import re
-		units = {u[0]:u for u in self.proto.coin_amt.units}
-		pat = re.compile(r'([1-9][0-9]*)({})'.format('|'.join(units)))
-		if pat.match(fee_arg):
-			amt, unit = pat.match(fee_arg).groups()
-			return self.fee_rel2abs(tx_size, units, int(amt), unit)
+		if res := parse_fee_spec(self.proto, fee_arg):
+			return self.fee_rel2abs(tx_size, float(res.amt), res.unit)
 
 
 		return False
 		return False
 
 
-	def get_usr_fee_interactive(self, fee=None, desc='Starting'):
+	def get_usr_fee_interactive(self, fee=None, *, desc='Starting'):
 		abs_fee = None
 		abs_fee = None
 		from ..ui import line_input
 		from ..ui import line_input
 		while True:
 		while True:
@@ -164,7 +166,7 @@ class New(Base):
 			return False
 			return False
 		return True
 		return True
 
 
-	def add_output(self, coinaddr, amt, is_chg=False, is_vault=False, data=None):
+	def add_output(self, coinaddr, amt, *, is_chg=False, is_vault=False, data=None):
 		self.outputs.append(
 		self.outputs.append(
 			self.Output(self.proto, addr=coinaddr, amt=amt, is_chg=is_chg, is_vault=is_vault, data=data))
 			self.Output(self.proto, addr=coinaddr, amt=amt, is_chg=is_chg, is_vault=is_vault, data=data))
 
 
@@ -197,7 +199,7 @@ class New(Base):
 
 
 		return _pa(arg, mmid, coin_addr, amt, None, is_vault)
 		return _pa(arg, mmid, coin_addr, amt, None, is_vault)
 
 
-	async def get_autochg_addr(self, proto, arg, exclude, desc, all_addrtypes=False):
+	async def get_autochg_addr(self, proto, arg, *, exclude, desc, all_addrtypes=False):
 		from ..tw.addresses import TwAddresses
 		from ..tw.addresses import TwAddresses
 		al = await TwAddresses(self.cfg, proto, get_data=True)
 		al = await TwAddresses(self.cfg, proto, get_data=True)
 
 
@@ -287,7 +289,7 @@ class New(Base):
 		for addrfile in addrfiles:
 		for addrfile in addrfiles:
 			check_infile(addrfile)
 			check_infile(addrfile)
 			try:
 			try:
-				ad_f.add(AddrList(self.cfg, proto, addrfile))
+				ad_f.add(AddrList(self.cfg, proto, infile=addrfile))
 			except Exception as e:
 			except Exception as e:
 				msg(f'{type(e).__name__}: {e}')
 				msg(f'{type(e).__name__}: {e}')
 		return ad_f
 		return ad_f
@@ -385,7 +387,10 @@ class New(Base):
 			self.get_unspent_nums_from_user
 			self.get_unspent_nums_from_user
 		)(self.twuo.data)
 		)(self.twuo.data)
 
 
-		msg(f'Selected output{suf(sel_nums)}: {{}}'.format(' '.join(str(n) for n in sel_nums)))
+		msg('Selected {}{}: {}'.format(
+			self.twuo.item_desc,
+			suf(sel_nums),
+			' '.join(str(n) for n in sel_nums)))
 		sel_unspent = MMGenList(self.twuo.data[i-1] for i in sel_nums)
 		sel_unspent = MMGenList(self.twuo.data[i-1] for i in sel_nums)
 
 
 		if not await self.precheck_sufficient_funds(
 		if not await self.precheck_sufficient_funds(
@@ -400,12 +405,12 @@ class New(Base):
 	async def get_fee(self, fee, outputs_sum, start_fee_desc):
 	async def get_fee(self, fee, outputs_sum, start_fee_desc):
 
 
 		if fee:
 		if fee:
-			self.usr_fee = self.get_usr_fee_interactive(fee, start_fee_desc)
+			self.usr_fee = self.get_usr_fee_interactive(fee, desc=start_fee_desc)
 		else:
 		else:
 			fee_per_kb, fe_type = await self.get_rel_fee_from_network()
 			fee_per_kb, fe_type = await self.get_rel_fee_from_network()
 			self.usr_fee = self.get_usr_fee_interactive(
 			self.usr_fee = self.get_usr_fee_interactive(
-				None if fee_per_kb is None else self.fee_est2abs(fee_per_kb, fe_type),
-				self.network_estimated_fee_label)
+				None if fee_per_kb is None else self.fee_est2abs(fee_per_kb, fe_type=fe_type),
+				desc = self.network_estimated_fee_label)
 
 
 		funds = await self.get_funds_available(self.usr_fee, outputs_sum)
 		funds = await self.get_funds_available(self.usr_fee, outputs_sum)
 
 
@@ -419,14 +424,14 @@ class New(Base):
 		else:
 		else:
 			self.warn_insufficient_funds(funds.amt, self.coin)
 			self.warn_insufficient_funds(funds.amt, self.coin)
 
 
-	async def create(self, cmd_args, locktime=None, do_info=False, caller='txcreate'):
+	async def create(self, cmd_args, *, locktime=None, do_info=False, caller='txcreate'):
 
 
 		assert isinstance(locktime, (int, type(None))), 'locktime must be of type int'
 		assert isinstance(locktime, (int, type(None))), 'locktime must be of type int'
 
 
 		from ..tw.unspent import TwUnspentOutputs
 		from ..tw.unspent import TwUnspentOutputs
 
 
 		if self.cfg.comment_file:
 		if self.cfg.comment_file:
-			self.add_comment(self.cfg.comment_file)
+			self.add_comment(infile=self.cfg.comment_file)
 
 
 		if not do_info:
 		if not do_info:
 			cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
 			cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
@@ -478,10 +483,11 @@ class New(Base):
 				fee_hint = self.update_vault_output(
 				fee_hint = self.update_vault_output(
 					self.vault_output.amt or self.sum_inputs(),
 					self.vault_output.amt or self.sum_inputs(),
 					deduct_est_fee = self.vault_output == self.chg_output)
 					deduct_est_fee = self.vault_output == self.chg_output)
-			if funds_left := await self.get_fee(
+			desc = 'User-selected' if self.cfg.fee else 'Recommended' if fee_hint else None
+			if (funds_left := await self.get_fee(
 					self.cfg.fee or fee_hint,
 					self.cfg.fee or fee_hint,
 					outputs_sum,
 					outputs_sum,
-					'User-selected' if self.cfg.fee else 'Recommended' if fee_hint else None):
+					desc)) is not None:
 				break
 				break
 
 
 		self.check_non_mmgen_inputs(caller)
 		self.check_non_mmgen_inputs(caller)

+ 2 - 3
mmgen/tx/online.py

@@ -23,7 +23,7 @@ class OnlineSigned(Signed):
 
 
 	def check_swap_expiry(self):
 	def check_swap_expiry(self):
 		import time
 		import time
-		from ..util import msg, make_timestr, die
+		from ..util import msg, make_timestr
 		from ..util2 import format_elapsed_hr
 		from ..util2 import format_elapsed_hr
 		from ..color import pink, yellow
 		from ..color import pink, yellow
 		expiry = self.swap_quote_expiry
 		expiry = self.swap_quote_expiry
@@ -34,8 +34,7 @@ class OnlineSigned(Signed):
 			a = clr('expired' if t_rem < 0 else 'expires'),
 			a = clr('expired' if t_rem < 0 else 'expires'),
 			b = clr(format_elapsed_hr(expiry, now=now, future_msg='from now')),
 			b = clr(format_elapsed_hr(expiry, now=now, future_msg='from now')),
 			c = make_timestr(expiry)))
 			c = make_timestr(expiry)))
-		if t_rem < 0:
-			die(2, 'Swap quote has expired. Please re-create the transaction')
+		return t_rem >= 0
 
 
 	def confirm_send(self):
 	def confirm_send(self):
 		from ..util import msg
 		from ..util import msg

+ 6 - 6
mmgen/tx/sign.py

@@ -37,7 +37,7 @@ def get_seed_for_seed_id(sid, infiles, saved_seeds):
 	subseeds_checked = False
 	subseeds_checked = False
 	while True:
 	while True:
 		if infiles:
 		if infiles:
-			seed = Wallet(cfg, infiles.pop(0), ignore_in_fmt=True, passwd_file=global_passwd_file).seed
+			seed = Wallet(cfg, fn=infiles.pop(0), ignore_in_fmt=True, passwd_file=global_passwd_file).seed
 		elif subseeds_checked is False:
 		elif subseeds_checked is False:
 			seed = saved_seeds[list(saved_seeds)[0]].subseed_by_seed_id(sid, print_msg=True)
 			seed = saved_seeds[list(saved_seeds)[0]].subseed_by_seed_id(sid, print_msg=True)
 			subseeds_checked = True
 			subseeds_checked = True
@@ -77,7 +77,7 @@ def generate_kals_for_mmgen_addrs(need_keys, infiles, saved_seeds, proto):
 						skip_chksum = True)
 						skip_chksum = True)
 	return MMGenList(gen_kals())
 	return MMGenList(gen_kals())
 
 
-def add_keys(src, io_list, infiles=None, saved_seeds=None, keyaddr_list=None):
+def add_keys(src, io_list, infiles=None, saved_seeds=None, *, keyaddr_list=None):
 
 
 	need_keys = [e for e in io_list if e.mmid and not e.have_wif]
 	need_keys = [e for e in io_list if e.mmid and not e.have_wif]
 
 
@@ -128,7 +128,7 @@ def get_tx_files(cfg, args):
 		die(1, 'You must specify a raw transaction file!')
 		die(1, 'You must specify a raw transaction file!')
 	return ret
 	return ret
 
 
-def get_seed_files(cfg, args, ignore_dfl_wallet=False, empty_ok=False):
+def get_seed_files(cfg, args, *, ignore_dfl_wallet=False, empty_ok=False):
 	# favor unencrypted seed sources first, as they don't require passwords
 	# favor unencrypted seed sources first, as they don't require passwords
 	ret = _pop_matching_fns(args, get_wallet_extensions('unenc'))
 	ret = _pop_matching_fns(args, get_wallet_extensions('unenc'))
 	from ..filename import find_file_in_dir
 	from ..filename import find_file_in_dir
@@ -142,16 +142,16 @@ def get_seed_files(cfg, args, ignore_dfl_wallet=False, empty_ok=False):
 
 
 def get_keyaddrlist(cfg, proto):
 def get_keyaddrlist(cfg, proto):
 	if cfg.mmgen_keys_from_file:
 	if cfg.mmgen_keys_from_file:
-		return KeyAddrList(cfg, proto, cfg.mmgen_keys_from_file)
+		return KeyAddrList(cfg, proto, infile=cfg.mmgen_keys_from_file)
 	return None
 	return None
 
 
 def get_keylist(cfg):
 def get_keylist(cfg):
 	if cfg.keys_from_file:
 	if cfg.keys_from_file:
 		from ..fileutil import get_lines_from_file
 		from ..fileutil import get_lines_from_file
-		return get_lines_from_file(cfg, cfg.keys_from_file, 'key-address data', trim_comments=True)
+		return get_lines_from_file(cfg, cfg.keys_from_file, desc='key-address data', trim_comments=True)
 	return None
 	return None
 
 
-async def txsign(cfg_parm, tx, seed_files, kl, kal, tx_num_str='', passwd_file=None):
+async def txsign(cfg_parm, tx, seed_files, kl, kal, *, tx_num_str='', passwd_file=None):
 
 
 	keys = MMGenList() # list of AddrListEntry objects
 	keys = MMGenList() # list of AddrListEntry objects
 	non_mmaddrs = tx.get_non_mmaddrs('inputs')
 	non_mmaddrs = tx.get_non_mmaddrs('inputs')

+ 223 - 0
mmgen/tx/tx_proxy.py

@@ -0,0 +1,223 @@
+#!/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
+
+"""
+tx.tx_proxy: tx proxy classes
+"""
+
+from ..color import green, pink, orange
+from ..util import msg, msg_r, die
+
+class TxProxyClient:
+
+	proto = 'https'
+	verify = True
+	timeout = 60
+	http_hdrs = {
+		'User-Agent': 'curl/8.7.1',
+		'Proxy-Connection': 'Keep-Alive'}
+
+	def __init__(self, cfg):
+		self.cfg = cfg
+		import requests
+		self.session = requests.Session()
+		self.session.trust_env = False # ignore *_PROXY environment vars
+		self.session.headers = self.http_hdrs
+		if self.cfg.proxy:
+			self.session.proxies.update({
+				'http':  f'socks5h://{self.cfg.proxy}',
+				'https': f'socks5h://{self.cfg.proxy}'
+			})
+
+	def call(self, name, path, err_fs, timeout, *, data=None):
+		url = self.proto + '://' + self.host + path
+		kwargs = {
+			'url': url,
+			'timeout': timeout or self.timeout,
+			'verify': self.verify}
+		if data:
+			kwargs['data'] = data
+		res = getattr(self.session, name)(**kwargs)
+		if res.status_code != 200:
+			die(2, '\n' + err_fs.format(s=res.status_code, u=url, d=data))
+		return res.content.decode()
+
+	def get(self, *, path, timeout=None):
+		err_fs = 'HTTP Get failed with status code {s}\n  URL: {u}'
+		return self.call('get', path, err_fs, timeout)
+
+	def post(self, *, path, data, timeout=None):
+		err_fs = 'HTTP Post failed with status code {s}\n  URL: {u}\n  DATA: {d}'
+		return self.call('post', path, err_fs, timeout, data=data)
+
+	def get_form(self, timeout=None):
+		return self.get(path=self.form_path, timeout=timeout)
+
+	def post_form(self, *, data, timeout=None):
+		return self.post(path=self.form_path, data=data, timeout=timeout)
+
+	def get_form_element(self, text):
+		from lxml import html
+		root = html.document_fromstring(text)
+		res = [e for e in root.forms if e.attrib.get('action', '').endswith(self.form_path)]
+		assert res, 'no matching forms!'
+		assert len(res) == 1, 'more than one matching form!'
+		return res[0]
+
+	def cache_fn(self, desc):
+		return f'{self.name}-{desc}.html'
+
+	def save_response(self, data, desc):
+		from ..fileutil import write_data_to_file
+		write_data_to_file(
+			self.cfg,
+			self.cache_fn(desc),
+			data,
+			desc = f'{desc} page from {orange(self.host)}')
+
+class BlockchairTxProxyClient(TxProxyClient):
+
+	name = 'blockchair'
+	host = 'blockchair.com'
+	form_path = '/broadcast'
+	assets = {
+		'avax': 'avalanche',
+		'btc':  'bitcoin',
+		'bch':  'bitcoin-cash',
+		'bnb':  'bnb',
+		'dash': 'dash',
+		'doge': 'dogecoin',
+		'eth':  'ethereum',
+		'etc':  'ethereum-classic',
+		'ltc':  'litecoin',
+		'zec':  'zcash',
+	}
+	active_assets = () # tried with ETH, doesn’t work
+
+	def create_post_data(self, *, form_text, coin, tx_hex):
+
+		coin = coin.lower()
+		assert coin in self.assets, f'coin {coin} not supported by {self.name}'
+		asset = self.assets[coin]
+
+		form = self.get_form_element(form_text)
+		data = {}
+
+		e = form.find('.//input')
+		assert e.attrib['name'] == '_token', 'input name incorrect!'
+		data['_token'] = e.attrib['value']
+
+		e = form.find('.//textarea')
+		assert e.attrib['name'] == 'data', 'textarea name incorrect!'
+		data['data'] = '0x' + tx_hex
+
+		e = form.find('.//button')
+		assert e is not None, 'missing button!'
+
+		e = form.find('.//select')
+		assert e.attrib['name'] == 'blockchain', 'select element name incorrect!'
+
+		assets = [f.get('value') for f in e.iter() if f.get('value')]
+		assert asset in assets, f'coin {coin} ({asset}) not currently supported by {self.name}'
+
+		data['blockchain'] = asset
+
+		return data
+
+	def get_txid(self, *, result_text):
+		msg(f'Response parsing TBD.  Check the cached response at {self.cache_fn("result")}')
+
+class EtherscanTxProxyClient(TxProxyClient):
+	name = 'etherscan'
+	host = 'etherscan.io'
+	form_path = '/pushTx'
+	assets = {'eth': 'ethereum'}
+	active_assets = ('eth',)
+
+	def create_post_data(self, *, form_text, coin, tx_hex):
+
+		form = self.get_form_element(form_text)
+		data = {}
+
+		for e in form.findall('.//input'):
+			data[e.attrib['name']] = e.attrib['value']
+
+		if len(data) != 4:
+			msg('')
+			self.save_response(form_text, 'form')
+			die(3, f'{len(data)}: unexpected number of keys in data (expected 4)')
+
+		e = form.find('.//textarea')
+		data[e.attrib['name']] = '0x' + tx_hex
+
+		return data
+
+	def get_txid(self, *, result_text):
+		import json
+		from ..obj import CoinTxID, is_coin_txid
+		form = self.get_form_element(result_text)
+		json_text = form.find('div/div/div')[1].tail
+		txid = json.loads(json_text)['result'].removeprefix('0x')
+		if is_coin_txid(txid):
+			return CoinTxID(txid)
+		else:
+			return False
+
+def send_tx(cfg, tx):
+
+	c = get_client(cfg)
+	msg(f'Using {pink(cfg.tx_proxy.upper())} tx proxy')
+
+	if not cfg.test:
+		tx.confirm_send()
+
+	msg_r(f'Retrieving form from {orange(c.host)}...')
+	form_text = c.get_form(timeout=180)
+	msg('done')
+
+	msg_r('Parsing form...')
+	post_data = c.create_post_data(
+		form_text = form_text,
+		coin      = cfg.coin,
+		tx_hex    = tx.serialized)
+	msg('done')
+
+	if cfg.test:
+		msg(f'Form retrieved from {orange(c.host)} and parsed')
+		msg(green('Transaction can be sent'))
+		return False
+
+	msg_r('Sending data...')
+	result_text = c.post_form(data=post_data, timeout=180)
+	msg('done')
+
+	msg_r('Parsing response...')
+	txid = c.get_txid(result_text=result_text)
+	msg('done')
+
+	msg('Transaction ' + (f'sent: {txid.hl()}' if txid else 'send failed'))
+	c.save_response(result_text, 'result')
+
+	return bool(txid)
+
+tx_proxies = {
+	'blockchair': BlockchairTxProxyClient,
+	'etherscan':  EtherscanTxProxyClient
+}
+
+def get_client(cfg, *, check_only=False):
+	proxy = tx_proxies[cfg.tx_proxy]
+	if cfg.coin.lower() in proxy.active_assets:
+		return True if check_only else proxy(cfg)
+	else:
+		die(1, f'Coin {cfg.coin} not supported by TX proxy {pink(proxy.name.upper())}')
+
+def check_client(cfg):
+	return get_client(cfg, check_only=True)

+ 11 - 10
mmgen/ui.py

@@ -16,7 +16,7 @@ import sys, os
 
 
 from .util import msg, msg_r, Msg, die
 from .util import msg, msg_r, Msg, die
 
 
-def confirm_or_raise(cfg, message, action, expect='YES', exit_msg='Exiting at user request'):
+def confirm_or_raise(cfg, message, action, *, expect='YES', exit_msg='Exiting at user request'):
 	if message:
 	if message:
 		msg(message)
 		msg(message)
 	if line_input(
 	if line_input(
@@ -32,13 +32,13 @@ def get_words_from_user(cfg, prompt):
 		msg('Sanitized input: [{}]'.format(' '.join(words)))
 		msg('Sanitized input: [{}]'.format(' '.join(words)))
 	return words
 	return words
 
 
-def get_data_from_user(cfg, desc='data'): # user input MUST be UTF-8
+def get_data_from_user(cfg, *, desc='data'): # user input MUST be UTF-8
 	data = line_input(cfg, f'Enter {desc}: ', echo=cfg.echo_passphrase)
 	data = line_input(cfg, f'Enter {desc}: ', echo=cfg.echo_passphrase)
 	if cfg.debug:
 	if cfg.debug:
 		msg(f'User input: [{data}]')
 		msg(f'User input: [{data}]')
 	return data
 	return data
 
 
-def line_input(cfg, prompt, echo=True, insert_txt='', hold_protect=True):
+def line_input(cfg, prompt, *, echo=True, insert_txt='', hold_protect=True):
 	"""
 	"""
 	multi-line prompts OK
 	multi-line prompts OK
 	one-line prompts must begin at beginning of line
 	one-line prompts must begin at beginning of line
@@ -81,12 +81,13 @@ def line_input(cfg, prompt, echo=True, insert_txt='', hold_protect=True):
 	return reply.strip()
 	return reply.strip()
 
 
 def keypress_confirm(
 def keypress_confirm(
-	cfg,
-	prompt,
-	default_yes     = False,
-	verbose         = False,
-	no_nl           = False,
-	complete_prompt = False):
+		cfg,
+		prompt,
+		*,
+		default_yes     = False,
+		verbose         = False,
+		no_nl           = False,
+		complete_prompt = False):
 
 
 	if not complete_prompt:
 	if not complete_prompt:
 		prompt = '{} {}: '.format(prompt, '(Y/n)' if default_yes else '(y/N)')
 		prompt = '{} {}: '.format(prompt, '(Y/n)' if default_yes else '(y/N)')
@@ -133,7 +134,7 @@ def do_pager(text):
 		Msg(text+end_msg)
 		Msg(text+end_msg)
 	set_vt100()
 	set_vt100()
 
 
-def do_license_msg(cfg, immed=False):
+def do_license_msg(cfg, *, immed=False):
 
 
 	if cfg.quiet or cfg.no_license or cfg.yes or not cfg.stdin_tty:
 	if cfg.quiet or cfg.no_license or cfg.yes or not cfg.stdin_tty:
 		return
 		return

+ 17 - 16
mmgen/util.py

@@ -69,6 +69,7 @@ class Util:
 			desc1,
 			desc1,
 			chk2,
 			chk2,
 			desc2,
 			desc2,
+			*,
 			hdr         = '',
 			hdr         = '',
 			die_on_fail = False,
 			die_on_fail = False,
 			verbose     = False):
 			verbose     = False):
@@ -88,7 +89,7 @@ class Util:
 
 
 		return True
 		return True
 
 
-	def compare_or_die(self, val1, desc1, val2, desc2, e='Error'):
+	def compare_or_die(self, val1, desc1, val2, desc2, *, e='Error'):
 		if val1 != val2:
 		if val1 != val2:
 			die(3, f"{e}: {desc2} ({val2}) doesn't match {desc1} ({val1})")
 			die(3, f"{e}: {desc2} ({val2}) doesn't match {desc1} ({val1})")
 		if self.cfg.debug:
 		if self.cfg.debug:
@@ -156,7 +157,7 @@ def mdie(*args):
 	mmsg(*args)
 	mmsg(*args)
 	sys.exit(0)
 	sys.exit(0)
 
 
-def die(ev, s='', stdout=False):
+def die(ev, s='', *, stdout=False):
 	if isinstance(ev, int):
 	if isinstance(ev, int):
 		from .exception import MMGenSystemExit, MMGenError
 		from .exception import MMGenSystemExit, MMGenError
 		if ev <= 2:
 		if ev <= 2:
@@ -179,15 +180,15 @@ def pp_fmt(d):
 def pp_msg(d):
 def pp_msg(d):
 	msg(pp_fmt(d))
 	msg(pp_fmt(d))
 
 
-def indent(s, indent='    ', append='\n'):
+def indent(s, *, indent='    ', append='\n'):
 	"indent multiple lines of text with specified string"
 	"indent multiple lines of text with specified string"
 	return indent + ('\n'+indent).join(s.strip().splitlines()) + append
 	return indent + ('\n'+indent).join(s.strip().splitlines()) + append
 
 
-def fmt(s, indent='', strip_char=None, append='\n'):
+def fmt(s, *, indent='', strip_char=None, append='\n'):
 	"de-indent multiple lines of text, or indent with specified string"
 	"de-indent multiple lines of text, or indent with specified string"
 	return indent + ('\n'+indent).join([l.lstrip(strip_char) for l in s.strip().splitlines()]) + append
 	return indent + ('\n'+indent).join([l.lstrip(strip_char) for l in s.strip().splitlines()]) + append
 
 
-def fmt_list(iterable, fmt='dfl', indent='', conv=None):
+def fmt_list(iterable, *, fmt='dfl', indent='', conv=None):
 	"pretty-format a list"
 	"pretty-format a list"
 	_conv, sep, lq, rq = {
 	_conv, sep, lq, rq = {
 		'dfl':       (str,  ", ", "'",  "'"),
 		'dfl':       (str,  ", ", "'",  "'"),
@@ -206,7 +207,7 @@ def fmt_list(iterable, fmt='dfl', indent='', conv=None):
 	conv = conv or _conv
 	conv = conv or _conv
 	return indent + (sep+indent).join(lq+conv(e)+rq for e in iterable)
 	return indent + (sep+indent).join(lq+conv(e)+rq for e in iterable)
 
 
-def fmt_dict(mapping, fmt='dfl', kconv=None, vconv=None):
+def fmt_dict(mapping, *, fmt='dfl', kconv=None, vconv=None):
 	"pretty-format a dict"
 	"pretty-format a dict"
 	kc, vc, sep, fs = {
 	kc, vc, sep, fs = {
 		'dfl':           (str, str,  ", ",  "'{}' ({})"),
 		'dfl':           (str, str,  ", ",  "'{}' ({})"),
@@ -242,7 +243,7 @@ def list_gen(*data):
 					yield d[idx]
 					yield d[idx]
 	return list(gen())
 	return list(gen())
 
 
-def remove_dups(iterable, edesc='element', desc='list', quiet=False, hide=False):
+def remove_dups(iterable, *, edesc='element', desc='list', quiet=False, hide=False):
 	"""
 	"""
 	Remove duplicate occurrences of iterable elements, preserving first occurrence
 	Remove duplicate occurrences of iterable elements, preserving first occurrence
 	If iterable is a generator, return a list, else type(iterable)
 	If iterable is a generator, return a list, else type(iterable)
@@ -259,7 +260,7 @@ def remove_dups(iterable, edesc='element', desc='list', quiet=False, hide=False)
 def contains_any(target_list, source_list):
 def contains_any(target_list, source_list):
 	return any(map(target_list.count, source_list))
 	return any(map(target_list.count, source_list))
 
 
-def suf(arg, suf_type='s', verb='none'):
+def suf(arg, suf_type='s', *, verb='none'):
 	suf_types = {
 	suf_types = {
 		'none': {
 		'none': {
 			's':   ('s',   ''),
 			's':   ('s',   ''),
@@ -292,7 +293,7 @@ def remove_extension(fn, ext):
 	a, b = os.path.splitext(fn)
 	a, b = os.path.splitext(fn)
 	return a if b[1:] == ext else fn
 	return a if b[1:] == ext else fn
 
 
-def make_chksum_N(s, nchars, sep=False, rounds=2, upper=True):
+def make_chksum_N(s, nchars, *, sep=False, rounds=2, upper=True):
 	if isinstance(s, str):
 	if isinstance(s, str):
 		s = s.encode()
 		s = s.encode()
 	from hashlib import sha256
 	from hashlib import sha256
@@ -306,7 +307,7 @@ def make_chksum_N(s, nchars, sep=False, rounds=2, upper=True):
 		assert 4 <= nchars <= 64, 'illegal ‘nchars’ value'
 		assert 4 <= nchars <= 64, 'illegal ‘nchars’ value'
 	return ret.upper() if upper else ret
 	return ret.upper() if upper else ret
 
 
-def make_chksum_8(s, sep=False):
+def make_chksum_8(s, *, sep=False):
 	from .obj import HexStr
 	from .obj import HexStr
 	from hashlib import sha256
 	from hashlib import sha256
 	s = HexStr(sha256(sha256(s).digest()).hexdigest()[:8].upper(), case='upper')
 	s = HexStr(sha256(sha256(s).digest()).hexdigest()[:8].upper(), case='upper')
@@ -360,7 +361,7 @@ def secs_to_ms(secs):
 def is_int(s): # actually is_nonnegative_int()
 def is_int(s): # actually is_nonnegative_int()
 	return set(str(s) or 'x') <= set(digits)
 	return set(str(s) or 'x') <= set(digits)
 
 
-def check_int_between(val, imin, imax, desc):
+def check_int_between(val, imin, imax, *, desc):
 	if not imin <= int(val) <= imax:
 	if not imin <= int(val) <= imax:
 		die(1, f'{val}: invalid value for {desc} (must be between {imin} and {imax})')
 		die(1, f'{val}: invalid value for {desc} (must be between {imin} and {imax})')
 	return int(val)
 	return int(val)
@@ -379,7 +380,7 @@ def is_utf8(s):
 	else:
 	else:
 		return True
 		return True
 
 
-def remove_whitespace(s, ws='\t\r\n '):
+def remove_whitespace(s, *, ws='\t\r\n '):
 	return s.translate(dict((ord(e), None) for e in ws))
 	return s.translate(dict((ord(e), None) for e in ws))
 
 
 def strip_comment(line):
 def strip_comment(line):
@@ -396,7 +397,7 @@ class oneshot_warning:
 
 
 	color = 'nocolor'
 	color = 'nocolor'
 
 
-	def __init__(self, div=None, fmt_args=[], reverse=False):
+	def __init__(self, *, div=None, fmt_args=[], reverse=False):
 		self.do(type(self), div, fmt_args, reverse)
 		self.do(type(self), div, fmt_args, reverse)
 
 
 	def do(self, wcls, div, fmt_args, reverse):
 	def do(self, wcls, div, fmt_args, reverse):
@@ -422,10 +423,10 @@ class oneshot_warning:
 
 
 class oneshot_warning_group(oneshot_warning):
 class oneshot_warning_group(oneshot_warning):
 
 
-	def __init__(self, wcls, div=None, fmt_args=[], reverse=False):
+	def __init__(self, wcls, *, div=None, fmt_args=[], reverse=False):
 		self.do(getattr(self, wcls), div, fmt_args, reverse)
 		self.do(getattr(self, wcls), div, fmt_args, reverse)
 
 
-def get_subclasses(cls, names=False):
+def get_subclasses(cls, *, names=False):
 	def gen(cls):
 	def gen(cls):
 		for i in cls.__subclasses__():
 		for i in cls.__subclasses__():
 			yield i
 			yield i
@@ -456,7 +457,7 @@ def exit_if_mswin(feature):
 	if sys.platform == 'win32':
 	if sys.platform == 'win32':
 		die(2, capfirst(feature) + ' not supported on the MSWin / MSYS2 platform')
 		die(2, capfirst(feature) + ' not supported on the MSWin / MSYS2 platform')
 
 
-def have_sudo(silent=False):
+def have_sudo(*, silent=False):
 	from subprocess import run, DEVNULL
 	from subprocess import run, DEVNULL
 	redir = DEVNULL if silent else None
 	redir = DEVNULL if silent else None
 	try:
 	try:

+ 24 - 8
mmgen/util2.py

@@ -33,7 +33,7 @@ def die_pause(ev=0, s=''):
 def cffi_override_fixup():
 def cffi_override_fixup():
 	from cffi import FFI
 	from cffi import FFI
 	class FFI_override:
 	class FFI_override:
-		def cdef(self, csource, override=False, packed=False, pack=None):
+		def cdef(self, csource, *, override=False, packed=False, pack=None):
 			self._cdef(csource, override=True, packed=packed, pack=pack)
 			self._cdef(csource, override=True, packed=packed, pack=pack)
 	FFI.cdef = FFI_override.cdef
 	FFI.cdef = FFI_override.cdef
 
 
@@ -92,7 +92,7 @@ bytespec_map = (
 	('E',  1152921504606846976),
 	('E',  1152921504606846976),
 )
 )
 
 
-def int2bytespec(n, spec, fmt, print_sym=True, strip=False, add_space=False):
+def int2bytespec(n, spec, fmt, *, print_sym=True, strip=False, add_space=False):
 
 
 	def spec2int(spec):
 	def spec2int(spec):
 		for k, v in bytespec_map:
 		for k, v in bytespec_map:
@@ -128,14 +128,21 @@ def parse_bytespec(nbytes):
 
 
 	die(1, f'{nbytes!r}: invalid byte specifier')
 	die(1, f'{nbytes!r}: invalid byte specifier')
 
 
-def format_elapsed_days_hr(t, now=None, cached={}):
+def format_elapsed_days_hr(t, *, now=None, cached={}):
 	e = int((now or time.time()) - t)
 	e = int((now or time.time()) - t)
 	if not e in cached:
 	if not e in cached:
 		days = abs(e) // 86400
 		days = abs(e) // 86400
 		cached[e] = f'{days} day{suf(days)} ' + ('ago' if e > 0 else 'in the future')
 		cached[e] = f'{days} day{suf(days)} ' + ('ago' if e > 0 else 'in the future')
 	return cached[e]
 	return cached[e]
 
 
-def format_elapsed_hr(t, now=None, cached={}, rel_now=True, show_secs=False, future_msg='in the future'):
+def format_elapsed_hr(
+		t,
+		*,
+		now        = None,
+		cached     = {},
+		rel_now    = True,
+		show_secs  = False,
+		future_msg = 'in the future'):
 	e = int((now or time.time()) - t)
 	e = int((now or time.time()) - t)
 	key = f'{e}:{rel_now}:{show_secs}'
 	key = f'{e}:{rel_now}:{show_secs}'
 	if not key in cached:
 	if not key in cached:
@@ -160,7 +167,7 @@ def format_elapsed_hr(t, now=None, cached={}, rel_now=True, show_secs=False, fut
 		cached[key] = ' '.join(f'{n} {desc}{suf(n)}' for desc, n in data if n) + add_suffix()
 		cached[key] = ' '.join(f'{n} {desc}{suf(n)}' for desc, n in data if n) + add_suffix()
 	return cached[key]
 	return cached[key]
 
 
-def pretty_format(s, width=80, pfx=''):
+def pretty_format(s, *, width=80, pfx=''):
 	out = []
 	out = []
 	while s:
 	while s:
 		if len(s) <= width:
 		if len(s) <= width:
@@ -171,7 +178,7 @@ def pretty_format(s, width=80, pfx=''):
 		s = s[i+1:]
 		s = s[i+1:]
 	return pfx + ('\n'+pfx).join(out)
 	return pfx + ('\n'+pfx).join(out)
 
 
-def block_format(data, gw=2, cols=8, line_nums=None, data_is_hex=False):
+def block_format(data, *, gw=2, cols=8, line_nums=None, data_is_hex=False):
 	assert line_nums in (None, 'hex', 'dec'), "'line_nums' must be one of None, 'hex' or 'dec'"
 	assert line_nums in (None, 'hex', 'dec'), "'line_nums' must be one of None, 'hex' or 'dec'"
 	ln_fs = '{:06x}: ' if line_nums == 'hex' else '{:06}: '
 	ln_fs = '{:06x}: ' if line_nums == 'hex' else '{:06}: '
 	bytes_per_chunk = gw
 	bytes_per_chunk = gw
@@ -185,8 +192,8 @@ def block_format(data, gw=2, cols=8, line_nums=None, data_is_hex=False):
 			for i in range(nchunks)
 			for i in range(nchunks)
 	).rstrip() + '\n'
 	).rstrip() + '\n'
 
 
-def pretty_hexdump(data, gw=2, cols=8, line_nums=None):
-	return block_format(data.hex(), gw, cols, line_nums, data_is_hex=True)
+def pretty_hexdump(data, *, gw=2, cols=8, line_nums=None):
+	return block_format(data.hex(), gw=gw, cols=cols, line_nums=line_nums, data_is_hex=True)
 
 
 def decode_pretty_hexdump(data):
 def decode_pretty_hexdump(data):
 	pat = re.compile(fr'^[{hexdigits}]+:\s+')
 	pat = re.compile(fr'^[{hexdigits}]+:\s+')
@@ -213,6 +220,15 @@ def cliargs_convert(args):
 
 
 	return tuple(gen())
 	return tuple(gen())
 
 
+def port_in_use(port):
+	import socket
+	try:
+		socket.create_connection(('localhost', port)).close()
+	except:
+		return False
+	else:
+		return True
+
 class ExpInt(int):
 class ExpInt(int):
 	'encode or parse an integer in exponential notation with specified precision'
 	'encode or parse an integer in exponential notation with specified precision'
 
 

+ 3 - 0
mmgen/wallet/__init__.py

@@ -47,6 +47,7 @@ _wd('words',        'MMGenMnemonic',     'mmwords', 'mnemonic',   False, ('mmwor
 }
 }
 
 
 def get_wallet_data(
 def get_wallet_data(
+		*,
 		wtype       = None,
 		wtype       = None,
 		fmt_code    = None,
 		fmt_code    = None,
 		ext         = None,
 		ext         = None,
@@ -73,6 +74,7 @@ def get_wallet_data(
 
 
 def get_wallet_cls(
 def get_wallet_cls(
 		wtype       = None,
 		wtype       = None,
+		*,
 		fmt_code    = None,
 		fmt_code    = None,
 		ext         = None,
 		ext         = None,
 		die_on_fail = False):
 		die_on_fail = False):
@@ -111,6 +113,7 @@ def _get_me(modname):
 
 
 def Wallet(
 def Wallet(
 	cfg,
 	cfg,
+	*,
 	fn            = None,
 	fn            = None,
 	ss            = None,
 	ss            = None,
 	seed_bin      = None,
 	seed_bin      = None,

+ 5 - 5
mmgen/wallet/base.py

@@ -40,7 +40,7 @@ class wallet(MMGenObject, metaclass=WalletMeta):
 	class WalletData(MMGenObject):
 	class WalletData(MMGenObject):
 		pass
 		pass
 
 
-	def __init__(self,
+	def __init__(self, *,
 		in_data       = None,
 		in_data       = None,
 		passwd_file   = None):
 		passwd_file   = None):
 
 
@@ -79,7 +79,7 @@ class wallet(MMGenObject, metaclass=WalletMeta):
 			self.fmt_data = get_data_from_file(
 			self.fmt_data = get_data_from_file(
 				self.cfg,
 				self.cfg,
 				self.infile.name,
 				self.infile.name,
-				self.desc,
+				desc   = self.desc,
 				binary = self.file_mode=='binary')
 				binary = self.file_mode=='binary')
 		elif self.in_data:
 		elif self.in_data:
 			self.fmt_data = self.in_data
 			self.fmt_data = self.in_data
@@ -88,7 +88,7 @@ class wallet(MMGenObject, metaclass=WalletMeta):
 
 
 	def _get_data_from_user(self, desc):
 	def _get_data_from_user(self, desc):
 		from ..ui import get_data_from_user
 		from ..ui import get_data_from_user
-		return get_data_from_user(self.cfg, desc)
+		return get_data_from_user(self.cfg, desc=desc)
 
 
 	def _deformat_once(self):
 	def _deformat_once(self):
 		self._get_data()
 		self._get_data()
@@ -110,7 +110,7 @@ class wallet(MMGenObject, metaclass=WalletMeta):
 		self._format()
 		self._format()
 		return self.fmt_data
 		return self.fmt_data
 
 
-	def write_to_file(self, outdir='', desc=''):
+	def write_to_file(self, *, outdir='', desc=''):
 		self._format()
 		self._format()
 		kwargs = {
 		kwargs = {
 			'desc':     desc or self.desc,
 			'desc':     desc or self.desc,
@@ -130,7 +130,7 @@ class wallet(MMGenObject, metaclass=WalletMeta):
 			self.fmt_data,
 			self.fmt_data,
 			**kwargs)
 			**kwargs)
 
 
-	def check_usr_seed_len(self, bitlen=None):
+	def check_usr_seed_len(self, *, bitlen=None):
 		chk = bitlen or self.seed.bitlen
 		chk = bitlen or self.seed.bitlen
 		if self.cfg.seed_len and self.cfg.seed_len != chk:
 		if self.cfg.seed_len and self.cfg.seed_len != chk:
 			die(1, f'ERROR: requested seed length ({self.cfg.seed_len}) doesn’t match seed length of source ({chk})')
 			die(1, f'ERROR: requested seed length ({self.cfg.seed_len}) doesn’t match seed length of source ({chk})')

+ 1 - 1
mmgen/wallet/brain.py

@@ -50,7 +50,7 @@ class wallet(wallet):
 			d.hash_preset,
 			d.hash_preset,
 			buflen = bw_seed_len // 8)
 			buflen = bw_seed_len // 8)
 		self.cfg._util.qmsg('Done')
 		self.cfg._util.qmsg('Done')
-		self.seed = Seed(self.cfg, seed)
+		self.seed = Seed(self.cfg, seed_bin=seed)
 		msg(f'Seed ID: {self.seed.sid}')
 		msg(f'Seed ID: {self.seed.sid}')
 		self.cfg._util.qmsg('Check this value against your records')
 		self.cfg._util.qmsg('Check this value against your records')
 		return True
 		return True

+ 2 - 2
mmgen/wallet/dieroll.py

@@ -60,7 +60,7 @@ class wallet(wallet):
 					desc       = 'gathered from your die rolls')
 					desc       = 'gathered from your die rolls')
 				self.desc += ' plus user-supplied entropy'
 				self.desc += ' plus user-supplied entropy'
 
 
-		self.seed = Seed(self.cfg, seed_bytes)
+		self.seed = Seed(self.cfg, seed_bin=seed_bytes)
 
 
 		self.check_usr_seed_len()
 		self.check_usr_seed_len()
 		return True
 		return True
@@ -69,7 +69,7 @@ class wallet(wallet):
 
 
 		if not self.cfg.stdin_tty:
 		if not self.cfg.stdin_tty:
 			from ..ui import get_data_from_user
 			from ..ui import get_data_from_user
-			return get_data_from_user(self.cfg, desc)
+			return get_data_from_user(self.cfg, desc=desc)
 
 
 		bc = baseconv('b6d')
 		bc = baseconv('b6d')
 
 

+ 3 - 3
mmgen/wallet/enc.py

@@ -31,7 +31,7 @@ class wallet(wallet):
 				die(2, 'Passphrase from password file, so exiting')
 				die(2, 'Passphrase from password file, so exiting')
 			msg('Trying again...')
 			msg('Trying again...')
 
 
-	def _get_hash_preset_from_user(self, old_preset, add_desc=''):
+	def _get_hash_preset_from_user(self, old_preset, *, add_desc=''):
 		prompt = 'Enter {}hash preset for {}{}{},\nor hit ENTER to {} value ({!r}): '.format(
 		prompt = 'Enter {}hash preset for {}{}{},\nor hit ENTER to {} value ({!r}): '.format(
 			('old ' if self.op=='pwchg_old' else 'new ' if self.op=='pwchg_new' else ''),
 			('old ' if self.op=='pwchg_old' else 'new ' if self.op=='pwchg_new' else ''),
 			('', 'new ')[self.op=='new'],
 			('', 'new ')[self.op=='new'],
@@ -41,7 +41,7 @@ class wallet(wallet):
 			old_preset)
 			old_preset)
 		return self.crypto.get_hash_preset_from_user(old_preset=old_preset, prompt=prompt)
 		return self.crypto.get_hash_preset_from_user(old_preset=old_preset, prompt=prompt)
 
 
-	def _get_hash_preset(self, add_desc=''):
+	def _get_hash_preset(self, *, add_desc=''):
 		if hasattr(self, 'ss_in') and hasattr(self.ss_in.ssdata, 'hash_preset'):
 		if hasattr(self, 'ss_in') and hasattr(self.ss_in.ssdata, 'hash_preset'):
 			old_hp = self.ss_in.ssdata.hash_preset
 			old_hp = self.ss_in.ssdata.hash_preset
 			if self.cfg.keep_hash_preset:
 			if self.cfg.keep_hash_preset:
@@ -71,7 +71,7 @@ class wallet(wallet):
 			passwd_file = self.passwd_file,
 			passwd_file = self.passwd_file,
 			pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase')
 			pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase')
 
 
-	def _get_passphrase(self, add_desc=''):
+	def _get_passphrase(self, *, add_desc=''):
 		return self.crypto.get_passphrase(
 		return self.crypto.get_passphrase(
 			data_desc = self.desc + (f' {add_desc}' if add_desc else ''),
 			data_desc = self.desc + (f' {add_desc}' if add_desc else ''),
 			passwd_file = self.passwd_file,
 			passwd_file = self.passwd_file,

+ 3 - 3
mmgen/wallet/incog_base.py

@@ -142,9 +142,9 @@ class wallet(wallet):
 			return False
 			return False
 
 
 	def _verify_seed_oldfmt(self, seed):
 	def _verify_seed_oldfmt(self, seed):
-		m = f'Seed ID: {make_chksum_8(seed)}.  Is the Seed ID correct?'
+		prompt = f'Seed ID: {make_chksum_8(seed)}.  Is the Seed ID correct?'
 		from ..ui import keypress_confirm
 		from ..ui import keypress_confirm
-		if keypress_confirm(self.cfg, m, True):
+		if keypress_confirm(self.cfg, prompt, default_yes=True):
 			return seed
 			return seed
 		else:
 		else:
 			return False
 			return False
@@ -189,7 +189,7 @@ class wallet(wallet):
 				key_id   = ''))
 				key_id   = ''))
 
 
 		if seed:
 		if seed:
-			self.seed = Seed(self.cfg, seed)
+			self.seed = Seed(self.cfg, seed_bin=seed)
 			msg(f'Seed ID: {self.seed.sid}')
 			msg(f'Seed ID: {self.seed.sid}')
 			return True
 			return True
 		else:
 		else:

+ 5 - 5
mmgen/wallet/mmgen.py

@@ -33,7 +33,7 @@ class wallet(wallet):
 		super().__init__(*args, **kwargs)
 		super().__init__(*args, **kwargs)
 
 
 	# logic identical to _get_hash_preset_from_user()
 	# logic identical to _get_hash_preset_from_user()
-	def _get_label_from_user(self, old_lbl=''):
+	def _get_label_from_user(self, *, old_lbl=''):
 		prompt = 'Enter a wallet label, or hit ENTER {}: '.format(
 		prompt = 'Enter a wallet label, or hit ENTER {}: '.format(
 			'to reuse the label {}'.format(old_lbl.hl2(encl='‘’')) if old_lbl else
 			'to reuse the label {}'.format(old_lbl.hl2(encl='‘’')) if old_lbl else
 			'for no label')
 			'for no label')
@@ -60,7 +60,7 @@ class wallet(wallet):
 				lbl = self.label
 				lbl = self.label
 				self.cfg._util.qmsg('Using user-configured label {}'.format(lbl.hl2(encl='‘’')))
 				self.cfg._util.qmsg('Using user-configured label {}'.format(lbl.hl2(encl='‘’')))
 			else: # Prompt, using old value as default
 			else: # Prompt, using old value as default
-				lbl = self._get_label_from_user(old_lbl)
+				lbl = self._get_label_from_user(old_lbl=old_lbl)
 			if (not self.cfg.keep_label) and self.op == 'pwchg_new':
 			if (not self.cfg.keep_label) and self.op == 'pwchg_new':
 				self.cfg._util.qmsg('Label {}'.format('unchanged' if lbl == old_lbl else f'changed to {lbl!r}'))
 				self.cfg._util.qmsg('Label {}'.format('unchanged' if lbl == old_lbl else f'changed to {lbl!r}'))
 		elif self.label:
 		elif self.label:
@@ -122,7 +122,7 @@ class wallet(wallet):
 		d1, d2, d3, d4, d5 = lines[2].split()
 		d1, d2, d3, d4, d5 = lines[2].split()
 		d.seed_id = d1.upper()
 		d.seed_id = d1.upper()
 		d.key_id  = d2.upper()
 		d.key_id  = d2.upper()
-		self.check_usr_seed_len(int(d3))
+		self.check_usr_seed_len(bitlen=int(d3))
 		d.pw_status, d.timestamp = d4, d5
 		d.pw_status, d.timestamp = d4, d5
 
 
 		hpdata = lines[3].split()
 		hpdata = lines[3].split()
@@ -171,9 +171,9 @@ class wallet(wallet):
 		d.passwd = self._get_passphrase(
 		d.passwd = self._get_passphrase(
 			add_desc = os.path.basename(self.infile.name) if self.cfg.quiet else '')
 			add_desc = os.path.basename(self.infile.name) if self.cfg.quiet else '')
 		key = self.crypto.make_key(d.passwd, d.salt, d.hash_preset)
 		key = self.crypto.make_key(d.passwd, d.salt, d.hash_preset)
-		ret = self.crypto.decrypt_seed(d.enc_seed, key, d.seed_id, d.key_id)
+		ret = self.crypto.decrypt_seed(d.enc_seed, key, seed_id=d.seed_id, key_id=d.key_id)
 		if ret:
 		if ret:
-			self.seed = Seed(self.cfg, ret)
+			self.seed = Seed(self.cfg, seed_bin=ret)
 			return True
 			return True
 		else:
 		else:
 			return False
 			return False

+ 1 - 1
mmgen/wallet/mmhex.py

@@ -54,7 +54,7 @@ class wallet(wallet):
 		if not self.cfg._util.compare_chksums(chk, 'file', make_chksum_6(hstr), 'computed', verbose=True):
 		if not self.cfg._util.compare_chksums(chk, 'file', make_chksum_6(hstr), 'computed', verbose=True):
 			return False
 			return False
 
 
-		self.seed = Seed(self.cfg, bytes.fromhex(hstr))
+		self.seed = Seed(self.cfg, seed_bin=bytes.fromhex(hstr))
 		self.ssdata.chksum = chk
 		self.ssdata.chksum = chk
 
 
 		self.check_usr_seed_len()
 		self.check_usr_seed_len()

+ 6 - 6
mmgen/wallet/mnemonic.py

@@ -32,7 +32,7 @@ class wallet(wallet):
 
 
 		if not self.cfg.stdin_tty:
 		if not self.cfg.stdin_tty:
 			from ..ui import get_data_from_user
 			from ..ui import get_data_from_user
-			return get_data_from_user(self.cfg, desc)
+			return get_data_from_user(self.cfg, desc=desc)
 
 
 		self._print_seed_type()
 		self._print_seed_type()
 
 
@@ -53,8 +53,8 @@ class wallet(wallet):
 		seed = self.seed.data
 		seed = self.seed.data
 
 
 		bc = self.conv_cls(self.wl_id)
 		bc = self.conv_cls(self.wl_id)
-		mn  = bc.frombytes(seed, 'seed')
-		rev = bc.tobytes(mn, 'seed')
+		mn  = bc.frombytes(seed, pad='seed')
+		rev = bc.tobytes(mn, pad='seed')
 
 
 		# Internal error, so just die on fail
 		# Internal error, so just die on fail
 		self.cfg._util.compare_or_die(rev, 'recomputed seed', seed, 'original seed', e='Internal error')
 		self.cfg._util.compare_or_die(rev, 'recomputed seed', seed, 'original seed', e='Internal error')
@@ -78,8 +78,8 @@ class wallet(wallet):
 				msg(f'Invalid mnemonic: word #{n} is not in the {self.wl_id.upper()} wordlist')
 				msg(f'Invalid mnemonic: word #{n} is not in the {self.wl_id.upper()} wordlist')
 				return False
 				return False
 
 
-		seed = bc.tobytes(mn, 'seed')
-		rev  = bc.frombytes(seed, 'seed')
+		seed = bc.tobytes(mn, pad='seed')
+		rev  = bc.frombytes(seed, pad='seed')
 
 
 		if len(seed) * 8 not in Seed.lens:
 		if len(seed) * 8 not in Seed.lens:
 			msg('Invalid mnemonic (produces too large a number)')
 			msg('Invalid mnemonic (produces too large a number)')
@@ -93,7 +93,7 @@ class wallet(wallet):
 			desc2 = 'original mnemonic',
 			desc2 = 'original mnemonic',
 			e     = 'Internal error')
 			e     = 'Internal error')
 
 
-		self.seed = Seed(self.cfg, seed)
+		self.seed = Seed(self.cfg, seed_bin=seed)
 		self.ssdata.mnemonic = mn
 		self.ssdata.mnemonic = mn
 
 
 		self.check_usr_seed_len()
 		self.check_usr_seed_len()

+ 1 - 1
mmgen/wallet/plainhex.py

@@ -36,7 +36,7 @@ class wallet(wallet):
 			msg(f'Invalid data length ({len(d)}) in {desc}')
 			msg(f'Invalid data length ({len(d)}) in {desc}')
 			return False
 			return False
 
 
-		self.seed = Seed(self.cfg, bytes.fromhex(d))
+		self.seed = Seed(self.cfg, seed_bin=bytes.fromhex(d))
 
 
 		self.check_usr_seed_len()
 		self.check_usr_seed_len()
 
 

+ 1 - 1
mmgen/wallet/seed.py

@@ -59,7 +59,7 @@ class wallet(wallet):
 			msg(f'Invalid base-58 encoded seed: {b}')
 			msg(f'Invalid base-58 encoded seed: {b}')
 			return False
 			return False
 
 
-		self.seed = Seed(self.cfg, ret)
+		self.seed = Seed(self.cfg, seed_bin=ret)
 		self.ssdata.chksum = a
 		self.ssdata.chksum = a
 		self.ssdata.b58seed = b
 		self.ssdata.b58seed = b
 
 

+ 2 - 2
mmgen/xmrseed.py

@@ -46,7 +46,7 @@ class xmrseed(baseconv):
 		wstr = ''.join(word[:3] for word in words)
 		wstr = ''.join(word[:3] for word in words)
 		return words[crc32(wstr.encode()) % len(words)]
 		return words[crc32(wstr.encode()) % len(words)]
 
 
-	def tobytes(self, words_arg, pad=None):
+	def tobytes(self, words_arg, *, pad=None):
 
 
 		assert isinstance(words_arg, (list, tuple)), 'words must be list or tuple'
 		assert isinstance(words_arg, (list, tuple)), 'words must be list or tuple'
 		assert pad is None, f"{pad}: invalid 'pad' argument (must be None)"
 		assert pad is None, f"{pad}: invalid 'pad' argument (must be None)"
@@ -77,7 +77,7 @@ class xmrseed(baseconv):
 
 
 		return b''.join(gen())
 		return b''.join(gen())
 
 
-	def frombytes(self, bytestr, pad=None, tostr=False):
+	def frombytes(self, bytestr, *, pad=None, tostr=False):
 		assert pad is None, f"{pad}: invalid 'pad' argument (must be None)"
 		assert pad is None, f"{pad}: invalid 'pad' argument (must be None)"
 
 
 		desc = self.desc.short
 		desc = self.desc.short

+ 1 - 1
mmgen/xmrwallet/__init__.py

@@ -113,5 +113,5 @@ def op_cls(op_name):
 	cls.name = op_name
 	cls.name = op_name
 	return cls
 	return cls
 
 
-def op(op, cfg, infile, wallets, spec=None):
+def op(op, cfg, infile, wallets, *, spec=None):
 	return op_cls(op.replace('-', '_'))(cfg, uargs(infile, wallets, spec))
 	return op_cls(op.replace('-', '_'))(cfg, uargs(infile, wallets, spec))

+ 4 - 4
mmgen/xmrwallet/file/__init__.py

@@ -21,7 +21,7 @@ class MoneroMMGenFile:
 
 
 	silent_load = False
 	silent_load = False
 
 
-	def make_chksum(self, keys=None):
+	def make_chksum(self, *, keys=None):
 		res = json.dumps(
 		res = json.dumps(
 			dict((k, v) for k, v in self.data._asdict().items() if (not keys or k in keys)),
 			dict((k, v) for k, v in self.data._asdict().items() if (not keys or k in keys)),
 			cls = json_encoder
 			cls = json_encoder
@@ -30,11 +30,11 @@ class MoneroMMGenFile:
 
 
 	@property
 	@property
 	def base_chksum(self):
 	def base_chksum(self):
-		return self.make_chksum(self.base_chksum_fields)
+		return self.make_chksum(keys=self.base_chksum_fields)
 
 
 	@property
 	@property
 	def full_chksum(self):
 	def full_chksum(self):
-		return self.make_chksum(self.full_chksum_fields) if self.full_chksum_fields else None
+		return self.make_chksum(keys=self.full_chksum_fields) if self.full_chksum_fields else None
 
 
 	def check_checksums(self, d_wrap):
 	def check_checksums(self, d_wrap):
 		for k in ('base_chksum', 'full_chksum'):
 		for k in ('base_chksum', 'full_chksum'):
@@ -60,5 +60,5 @@ class MoneroMMGenFile:
 
 
 	def extract_data_from_file(self, cfg, fn):
 	def extract_data_from_file(self, cfg, fn):
 		return json.loads(
 		return json.loads(
-			get_data_from_file(cfg, str(fn), self.desc, silent=self.silent_load)
+			get_data_from_file(cfg, str(fn), desc=self.desc, silent=self.silent_load)
 		)[self.data_label]
 		)[self.data_label]

+ 5 - 5
mmgen/xmrwallet/file/outputs.py

@@ -45,7 +45,7 @@ class MoneroWalletOutputsFile:
 			self.name = type(self).__name__
 			self.name = type(self).__name__
 			self.cfg = cfg
 			self.cfg = cfg
 
 
-		def write(self, add_suf='', quiet=False):
+		def write(self, *, add_suf='', quiet=False):
 			from ...fileutil import write_data_to_file
 			from ...fileutil import write_data_to_file
 			write_data_to_file(
 			write_data_to_file(
 				cfg               = self.cfg,
 				cfg               = self.cfg,
@@ -71,7 +71,7 @@ class MoneroWalletOutputsFile:
 			)
 			)
 			return fn.parent / fn.name[:-(len(self.ext)+self.ext_offset+1)]
 			return fn.parent / fn.name[:-(len(self.ext)+self.ext_offset+1)]
 
 
-		def get_info(self, indent=''):
+		def get_info(self, *, indent=''):
 			if self.data.signed_key_images is not None:
 			if self.data.signed_key_images is not None:
 				data = self.data.signed_key_images or []
 				data = self.data.signed_key_images or []
 				return f'{indent}{self.wallet_fn.name}: {len(data)} signed key image{suf(data)}'
 				return f'{indent}{self.wallet_fn.name}: {len(data)} signed key image{suf(data)}'
@@ -81,7 +81,7 @@ class MoneroWalletOutputsFile:
 	class New(Base):
 	class New(Base):
 		ext = 'raw'
 		ext = 'raw'
 
 
-		def __init__(self, parent, wallet_fn, data, wallet_idx=None, sign=False):
+		def __init__(self, parent, wallet_fn, data, *, wallet_idx=None, sign=False):
 			super().__init__(parent.cfg)
 			super().__init__(parent.cfg)
 			self.wallet_fn = wallet_fn
 			self.wallet_fn = wallet_fn
 			init_data = dict.fromkeys(self.data_tuple._fields)
 			init_data = dict.fromkeys(self.data_tuple._fields)
@@ -96,7 +96,7 @@ class MoneroWalletOutputsFile:
 
 
 	class Completed(New):
 	class Completed(New):
 
 
-		def __init__(self, parent, fn=None, wallet_fn=None):
+		def __init__(self, parent, *, fn=None, wallet_fn=None):
 			def check_equal(desc, a, b):
 			def check_equal(desc, a, b):
 				assert a == b, f'{desc} mismatch: {a} (from file) != {b} (from filename)'
 				assert a == b, f'{desc} mismatch: {a} (from file) != {b} (from filename)'
 			fn = fn or self.get_outfile(parent.cfg, wallet_fn)
 			fn = fn or self.get_outfile(parent.cfg, wallet_fn)
@@ -115,7 +115,7 @@ class MoneroWalletOutputsFile:
 			self.check_checksums(d_wrap)
 			self.check_checksums(d_wrap)
 
 
 		@classmethod
 		@classmethod
-		def find_fn_from_wallet_fn(cls, cfg, wallet_fn, ret_on_no_match=False):
+		def find_fn_from_wallet_fn(cls, cfg, wallet_fn, *, ret_on_no_match=False):
 			path = get_autosign_obj(cfg).xmr_outputs_dir or Path()
 			path = get_autosign_obj(cfg).xmr_outputs_dir or Path()
 			pat = cls.fn_fs.format(
 			pat = cls.fn_fs.format(
 				a = wallet_fn.name,
 				a = wallet_fn.name,

+ 6 - 6
mmgen/xmrwallet/file/tx.py

@@ -84,7 +84,7 @@ class MoneroMMGenTX:
 		def src_wallet_idx(self):
 		def src_wallet_idx(self):
 			return int(self.data.source.split(':')[0])
 			return int(self.data.source.split(':')[0])
 
 
-		def get_info_oneline(self, indent='', addr_w=None):
+		def get_info_oneline(self, *, indent='', addr_w=None):
 			d = self.data
 			d = self.data
 			return self.oneline_fs.format(
 			return self.oneline_fs.format(
 					a = yellow(d.network),
 					a = yellow(d.network),
@@ -94,12 +94,12 @@ class MoneroMMGenTX:
 					e = purple(d.op.ljust(9)),
 					e = purple(d.op.ljust(9)),
 					f = red('{}:{}'.format(d.source.wallet, d.source.account).ljust(6)),
 					f = red('{}:{}'.format(d.source.wallet, d.source.account).ljust(6)),
 					g = red('{}:{}'.format(d.dest.wallet, d.dest.account).ljust(6)) if d.dest else cyan('ext   '),
 					g = red('{}:{}'.format(d.dest.wallet, d.dest.account).ljust(6)) if d.dest else cyan('ext   '),
-					h = d.amount.fmt(color=True, iwidth=4, prec=12),
-					j = d.dest_address.fmt(0, width=addr_w, color=True) if addr_w else d.dest_address.hl(0),
+					h = d.amount.fmt(4, color=True, prec=12),
+					j = d.dest_address.fmt(0, addr_w, color=True) if addr_w else d.dest_address.hl(0),
 					x = '->'
 					x = '->'
 				)
 				)
 
 
-		def get_info(self, indent='', addr_w=None):
+		def get_info(self, *, indent='', addr_w=None):
 			d = self.data
 			d = self.data
 			pmt_id = d.dest_address.parsed.payment_id
 			pmt_id = d.dest_address.parsed.payment_id
 			fs = '\n'.join(list_gen(
 			fs = '\n'.join(list_gen(
@@ -138,7 +138,7 @@ class MoneroMMGenTX:
 					F = (Int(d.priority).hl() + f' [{tx_priorities[d.priority]}]') if d.priority else None,
 					F = (Int(d.priority).hl() + f' [{tx_priorities[d.priority]}]') if d.priority else None,
 					n = d.fee.hl(),
 					n = d.fee.hl(),
 					o = d.dest_address.hl(0) if self.cfg.full_address
 					o = d.dest_address.hl(0) if self.cfg.full_address
-						else d.dest_address.fmt(0, width=addr_width, color=True),
+						else d.dest_address.fmt(0, addr_width, color=True),
 					P = pink(pmt_id.hex()) if pmt_id else None,
 					P = pink(pmt_id.hex()) if pmt_id else None,
 					s = make_timestr(d.submit_time) if d.submit_time else None,
 					s = make_timestr(d.submit_time) if d.submit_time else None,
 					S = pink(f" [cold signed{', submitted' if d.complete else ''}]") if d.signed_txset else '',
 					S = pink(f" [cold signed{', submitted' if d.complete else ''}]") if d.signed_txset else '',
@@ -153,7 +153,7 @@ class MoneroMMGenTX:
 		def file_id(self):
 		def file_id(self):
 			return (self.base_chksum + ('-' + self.full_chksum if self.full_chksum else '')).upper()
 			return (self.base_chksum + ('-' + self.full_chksum if self.full_chksum else '')).upper()
 
 
-		def write(self, delete_metadata=False, ask_write=True, ask_overwrite=True):
+		def write(self, *, delete_metadata=False, ask_write=True, ask_overwrite=True):
 			dict_data = self.data._asdict()
 			dict_data = self.data._asdict()
 			if delete_metadata:
 			if delete_metadata:
 				dict_data['metadata'] = None
 				dict_data['metadata'] = None

+ 2 - 2
mmgen/xmrwallet/include.py

@@ -19,7 +19,7 @@ from ..color import red, green, pink
 from ..addr import CoinAddr, AddrIdx
 from ..addr import CoinAddr, AddrIdx
 from ..util import die
 from ..util import die
 
 
-def gen_acct_addr_info(self, wallet_data, account, indent=''):
+def gen_acct_addr_info(self, wallet_data, account, *, indent=''):
 	fs = indent + '{I:<3} {A} {U} {B} {L}'
 	fs = indent + '{I:<3} {A} {U} {B} {L}'
 	addrs_data = wallet_data.addrs_data[account]['addresses']
 	addrs_data = wallet_data.addrs_data[account]['addresses']
 
 
@@ -47,7 +47,7 @@ def gen_acct_addr_info(self, wallet_data, account, indent=''):
 		from .ops import fmt_amt
 		from .ops import fmt_amt
 		yield fs.format(
 		yield fs.format(
 			I = addr['address_index'],
 			I = addr['address_index'],
-			A = ca.hl(0) if self.cfg.full_address else ca.fmt(0, color=True, width=addr_width),
+			A = ca.hl(0) if self.cfg.full_address else ca.fmt(0, addr_width, color=True),
 			U = (red('True ') if addr['used'] else green('False')),
 			U = (red('True ') if addr['used'] else green('False')),
 			B = fmt_amt(bal),
 			B = fmt_amt(bal),
 			L = pink(addr['label']))
 			L = pink(addr['label']))

+ 2 - 2
mmgen/xmrwallet/ops/__init__.py

@@ -51,7 +51,7 @@ class OpBase:
 		self.uargs = uarg_tuple
 		self.uargs = uarg_tuple
 
 
 		def fmt_amt(amt):
 		def fmt_amt(amt):
-			return self.proto.coin_amt(amt, from_unit='atomic').fmt(iwidth=5, prec=12, color=True)
+			return self.proto.coin_amt(amt, from_unit='atomic').fmt(5, prec=12, color=True)
 		def hl_amt(amt):
 		def hl_amt(amt):
 			return self.proto.coin_amt(amt, from_unit='atomic').hl()
 			return self.proto.coin_amt(amt, from_unit='atomic').hl()
 
 
@@ -102,7 +102,7 @@ class OpBase:
 			self.cfg.tx_relay_daemon,
 			self.cfg.tx_relay_daemon,
 			re.ASCII)
 			re.ASCII)
 
 
-	def display_tx_relay_info(self, indent=''):
+	def display_tx_relay_info(self, *, indent=''):
 		m = self.parse_tx_relay_opt()
 		m = self.parse_tx_relay_opt()
 		msg(fmt(f"""
 		msg(fmt(f"""
 			TX relay info:
 			TX relay info:

+ 1 - 1
mmgen/xmrwallet/ops/create.py

@@ -68,7 +68,7 @@ class OpCreateOffline(OpCreate):
 		vkal = ViewKeyAddrList(
 		vkal = ViewKeyAddrList(
 			cfg       = self.cfg,
 			cfg       = self.cfg,
 			proto     = self.proto,
 			proto     = self.proto,
-			addrfile  = None,
+			infile    = None,
 			addr_idxs = self.uargs.wallets,
 			addr_idxs = self.uargs.wallets,
 			seed      = self.seed_src.seed,
 			seed      = self.seed_src.seed,
 			skip_chksum_msg = True)
 			skip_chksum_msg = True)

+ 1 - 1
mmgen/xmrwallet/ops/import.py

@@ -23,7 +23,7 @@ class OpImportOutputs(OpWallet):
 	action = 'importing wallet outputs into'
 	action = 'importing wallet outputs into'
 	start_daemon = False
 	start_daemon = False
 
 
-	async def main(self, fn, wallet_idx, restart_daemon=True):
+	async def main(self, fn, wallet_idx, *, restart_daemon=True):
 		if restart_daemon:
 		if restart_daemon:
 			await self.restart_wallet_daemon()
 			await self.restart_wallet_daemon()
 		h = MoneroWalletRPC(self, self.addr_data[0])
 		h = MoneroWalletRPC(self, self.addr_data[0])

+ 1 - 1
mmgen/xmrwallet/ops/label.py

@@ -59,7 +59,7 @@ class OpLabel(OpMixinSpec, OpWallet):
 		from . import addr_width
 		from . import addr_width
 		msg('\n  {a} {b}\n  {c} {d}\n  {e} {f}'.format(
 		msg('\n  {a} {b}\n  {c} {d}\n  {e} {f}'.format(
 				a = 'Address:       ',
 				a = 'Address:       ',
-				b = ca.hl(0) if self.cfg.full_address else ca.fmt(0, color=True, width=addr_width),
+				b = ca.hl(0) if self.cfg.full_address else ca.fmt(0, addr_width, color=True),
 				c = 'Existing label:',
 				c = 'Existing label:',
 				d = pink(addr['label']) if addr['label'] else gray('[none]'),
 				d = pink(addr['label']) if addr['label'] else gray('[none]'),
 				e = 'New label:     ',
 				e = 'New label:     ',

+ 1 - 1
mmgen/xmrwallet/ops/relay.py

@@ -41,7 +41,7 @@ class OpRelay(OpBase):
 			md = None
 			md = None
 		else:
 		else:
 			from ...daemon import CoinDaemon
 			from ...daemon import CoinDaemon
-			md = CoinDaemon(self.cfg, 'xmr', test_suite=self.cfg.test_suite)
+			md = CoinDaemon(self.cfg, network_id='xmr', test_suite=self.cfg.test_suite)
 			host, port = ('localhost', md.rpc_port)
 			host, port = ('localhost', md.rpc_port)
 			proxy = None
 			proxy = None
 
 

+ 1 - 1
mmgen/xmrwallet/ops/sign.py

@@ -21,7 +21,7 @@ class OpSign(OpWallet):
 	action = 'signing transaction with'
 	action = 'signing transaction with'
 	start_daemon = False
 	start_daemon = False
 
 
-	async def main(self, fn, restart_daemon=True):
+	async def main(self, fn, *, restart_daemon=True):
 		if restart_daemon:
 		if restart_daemon:
 			await self.restart_wallet_daemon()
 			await self.restart_wallet_daemon()
 		tx = MoneroMMGenTX.Unsigned(self.cfg, fn)
 		tx = MoneroMMGenTX.Unsigned(self.cfg, fn)

+ 1 - 1
mmgen/xmrwallet/ops/txview.py

@@ -28,7 +28,7 @@ class OpTxview(OpBase):
 	footer = ''
 	footer = ''
 	do_umount = False
 	do_umount = False
 
 
-	async def main(self, cols=None):
+	async def main(self, *, cols=None):
 
 
 		self.mount_removable_device()
 		self.mount_removable_device()
 
 

+ 6 - 6
mmgen/xmrwallet/ops/wallet.py

@@ -102,9 +102,9 @@ class OpWallet(OpBase):
 			for first_try in (True, False):
 			for first_try in (True, False):
 				try:
 				try:
 					self.kal = (ViewKeyAddrList if (self.cfg.watch_only and first_try) else KeyAddrList)(
 					self.kal = (ViewKeyAddrList if (self.cfg.watch_only and first_try) else KeyAddrList)(
-						cfg      = cfg,
-						proto    = self.proto,
-						addrfile = str(self.autosign_viewkey_addr_file) if self.cfg.autosign else self.uargs.infile,
+						cfg    = cfg,
+						proto  = self.proto,
+						infile = str(self.autosign_viewkey_addr_file) if self.cfg.autosign else self.uargs.infile,
 						key_address_validity_check = True,
 						key_address_validity_check = True,
 						skip_chksum_msg = True)
 						skip_chksum_msg = True)
 					break
 					break
@@ -140,7 +140,7 @@ class OpWallet(OpBase):
 		return MoneroRPCClient(
 		return MoneroRPCClient(
 			cfg    = self.cfg,
 			cfg    = self.cfg,
 			proto  = self.proto,
 			proto  = self.proto,
-			daemon = CoinDaemon(self.cfg, 'xmr'),
+			daemon = CoinDaemon(self.cfg, network_id='xmr'),
 			host   = host,
 			host   = host,
 			port   = int(port),
 			port   = int(port),
 			user   = None,
 			user   = None,
@@ -163,7 +163,7 @@ class OpWallet(OpBase):
 
 
 	def create_addr_data(self):
 	def create_addr_data(self):
 		if self.uargs.wallets:
 		if self.uargs.wallets:
-			idxs = AddrIdxList(self.uargs.wallets)
+			idxs = AddrIdxList(fmt_str=self.uargs.wallets)
 			self.addr_data = [d for d in self.kal.data if d.idx in idxs]
 			self.addr_data = [d for d in self.kal.data if d.idx in idxs]
 			if len(self.addr_data) != len(idxs):
 			if len(self.addr_data) != len(idxs):
 				die(1, f'List {self.uargs.wallets!r} contains addresses not present in supplied key-address file')
 				die(1, f'List {self.uargs.wallets!r} contains addresses not present in supplied key-address file')
@@ -183,7 +183,7 @@ class OpWallet(OpBase):
 				self.c.daemon.force_kill = True
 				self.c.daemon.force_kill = True
 				self.c.daemon.stop()
 				self.c.daemon.stop()
 
 
-	def get_wallet_fn(self, data, watch_only=None):
+	def get_wallet_fn(self, data, *, watch_only=None):
 		if watch_only is None:
 		if watch_only is None:
 			watch_only = self.cfg.watch_only
 			watch_only = self.cfg.watch_only
 		return Path(
 		return Path(

+ 5 - 5
mmgen/xmrwallet/rpc.py

@@ -35,7 +35,7 @@ class MoneroWalletRPC:
 			MoneroMMGenTX.NewUnsigned if self.cfg.watch_only else
 			MoneroMMGenTX.NewUnsigned if self.cfg.watch_only else
 			MoneroMMGenTX.NewSigned)
 			MoneroMMGenTX.NewSigned)
 
 
-	def open_wallet(self, desc=None, refresh=True):
+	def open_wallet(self, desc=None, *, refresh=True):
 		add_desc = desc + ' ' if desc else self.parent.add_wallet_desc
 		add_desc = desc + ' ' if desc else self.parent.add_wallet_desc
 		gmsg_r(f'\n  Opening {add_desc}wallet...')
 		gmsg_r(f'\n  Opening {add_desc}wallet...')
 		self.c.call( # returns {}
 		self.c.call( # returns {}
@@ -62,7 +62,7 @@ class MoneroWalletRPC:
 		await self.c.stop_daemon(quiet=True) # closes wallet
 		await self.c.stop_daemon(quiet=True) # closes wallet
 		gmsg_r('done')
 		gmsg_r('done')
 
 
-	def gen_accts_info(self, accts_data, addrs_data, indent='    ', skip_empty_ok=False):
+	def gen_accts_info(self, accts_data, addrs_data, *, indent='    ', skip_empty_ok=False):
 		from .ops import addr_width
 		from .ops import addr_width
 		fs = indent + '  {I:<3} {A} {N} {B} {L}'
 		fs = indent + '  {I:<3} {A} {N} {B} {L}'
 		yield indent + f'Accounts of wallet {self.fn.name}:'
 		yield indent + f'Accounts of wallet {self.fn.name}:'
@@ -79,12 +79,12 @@ class MoneroWalletRPC:
 			from .ops import fmt_amt
 			from .ops import fmt_amt
 			yield fs.format(
 			yield fs.format(
 				I = str(e['account_index']),
 				I = str(e['account_index']),
-				A = ca.hl(0) if self.cfg.full_address else ca.fmt(0, color=True, width=addr_width),
+				A = ca.hl(0) if self.cfg.full_address else ca.fmt(0, addr_width, color=True),
 				N = red(str(len(addrs_data[i]['addresses'])).ljust(6)),
 				N = red(str(len(addrs_data[i]['addresses'])).ljust(6)),
 				B = fmt_amt(e['unlocked_balance']),
 				B = fmt_amt(e['unlocked_balance']),
 				L = pink(e['label']))
 				L = pink(e['label']))
 
 
-	def get_wallet_data(self, print=True, skip_empty_ok=False):
+	def get_wallet_data(self, *, print=True, skip_empty_ok=False):
 		accts_data = self.c.call('get_accounts')
 		accts_data = self.c.call('get_accounts')
 		addrs_data = [
 		addrs_data = [
 			self.c.call('get_address', account_index=i)
 			self.c.call('get_address', account_index=i)
@@ -123,7 +123,7 @@ class MoneroWalletRPC:
 		msg(cyan(ret['address']))
 		msg(cyan(ret['address']))
 		return ret['address']
 		return ret['address']
 
 
-	def get_last_addr(self, account, wallet_data, display=True):
+	def get_last_addr(self, account, wallet_data, *, display=True):
 		if display:
 		if display:
 			msg('\n    Getting last address:')
 			msg('\n    Getting last address:')
 		acct_addrs = wallet_data.addrs_data[account]['addresses']
 		acct_addrs = wallet_data.addrs_data[account]['addresses']

+ 1 - 0
nix/packages.nix

@@ -46,5 +46,6 @@
         semantic-version = semantic-version;
         semantic-version = semantic-version;
         pexpect          = pexpect;         # test suite
         pexpect          = pexpect;         # test suite
         pycoin           = pycoin;          # test suite
         pycoin           = pycoin;          # test suite
+        lxml             = lxml;
     };
     };
 }
 }

+ 9 - 0
pyproject.toml

@@ -6,6 +6,14 @@ requires = [
 ]
 ]
 build-backend = "setuptools.build_meta"
 build-backend = "setuptools.build_meta"
 
 
+[tool.ruff]
+line-length = 106
+indent-width = 4
+
+[tool.ruff.format]
+quote-style = "single"
+indent-style = "tab"
+
 [tool.ruff.lint]
 [tool.ruff.lint]
 ignore = [
 ignore = [
 	"E401", # multiple imports per line
 	"E401", # multiple imports per line
@@ -125,4 +133,5 @@ ignored-classes = [ # ignored for no-member, otherwise checked
 	"Opts",
 	"Opts",
 	"Help",
 	"Help",
 	"FFI_override",
 	"FFI_override",
+	"RPC",
 ]
 ]

+ 1 - 0
setup.cfg

@@ -58,6 +58,7 @@ install_requires =
 	aiohttp
 	aiohttp
 	requests
 	requests
 	pexpect
 	pexpect
+	lxml
 	scrypt; platform_system != "Windows" # must be installed by hand on MSYS2
 	scrypt; platform_system != "Windows" # must be installed by hand on MSYS2
 	semantic-version; platform_system != "Windows" # scripts/create-token.py
 	semantic-version; platform_system != "Windows" # scripts/create-token.py
 
 

+ 4 - 0
test/cmdtest_d/common.py

@@ -108,6 +108,7 @@ def get_file_with_ext(
 		no_dot      = False,
 		no_dot      = False,
 		return_list = False,
 		return_list = False,
 		delete_all  = False,
 		delete_all  = False,
+		subdir      = None,
 		substr      = False):
 		substr      = False):
 
 
 	dot = '' if no_dot else '.'
 	dot = '' if no_dot else '.'
@@ -118,6 +119,9 @@ def get_file_with_ext(
 			or fn.endswith(dot + ext)
 			or fn.endswith(dot + ext)
 			or (substr and ext in fn))
 			or (substr and ext in fn))
 
 
+	if subdir:
+		tdir = os.path.join(tdir, subdir)
+
 	# Don’t use os.scandir here - it returns broken paths under Windows/MSYS2
 	# Don’t use os.scandir here - it returns broken paths under Windows/MSYS2
 	flist = [os.path.join(tdir, name) for name in os.listdir(tdir) if have_match(name)]
 	flist = [os.path.join(tdir, name) for name in os.listdir(tdir) if have_match(name)]
 
 

+ 14 - 4
test/cmdtest_d/ct_automount.py

@@ -59,7 +59,9 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		('alice_txcreate4',                  'creating a transaction'),
 		('alice_txcreate4',                  'creating a transaction'),
 		('alice_txbump1',                    'bumping the unsigned transaction (error)'),
 		('alice_txbump1',                    'bumping the unsigned transaction (error)'),
 		('alice_txbump2',                    'bumping the unsent transaction (error)'),
 		('alice_txbump2',                    'bumping the unsent transaction (error)'),
-		('alice_txsend2',                    'sending the transaction'),
+		('alice_txsend2_dump_hex',           'dumping the transaction to hex'),
+		('alice_txsend2_cli',                'sending the transaction via cli'),
+		('alice_txsend2_mark_sent',          'marking the transaction sent'),
 		('alice_txbump3',                    'bumping the transaction'),
 		('alice_txbump3',                    'bumping the transaction'),
 		('alice_txsend3',                    'sending the bumped transaction'),
 		('alice_txsend3',                    'sending the bumped transaction'),
 		('alice_txbump4',                    'bumping the transaction (new outputs, fee too low)'),
 		('alice_txbump4',                    'bumping the transaction (new outputs, fee too low)'),
@@ -143,10 +145,18 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		return self.run_setup(mn_type='default', use_dfl_wallet=True, passwd=rt_pw)
 		return self.run_setup(mn_type='default', use_dfl_wallet=True, passwd=rt_pw)
 
 
 	def alice_txsend1(self):
 	def alice_txsend1(self):
-		return self._user_txsend('alice', 'This one’s worth a comment', no_wait=True)
+		return self._user_txsend('alice', comment='This one’s worth a comment', no_wait=True)
 
 
-	def alice_txsend2(self):
-		return self._user_txsend('alice', need_rbf=True)
+	def alice_txsend2_dump_hex(self):
+		return self._user_txsend('alice', need_rbf=True, dump_hex=True)
+
+	def alice_txsend2_cli(self):
+		if not self.proto.cap('rbf'):
+			return 'skip'
+		return self._user_dump_hex_send_cli('alice')
+
+	def alice_txsend2_mark_sent(self):
+		return self._user_txsend('alice', need_rbf=True, mark_sent=True)
 
 
 	def alice_txsend3(self):
 	def alice_txsend3(self):
 		return self._user_txsend('alice', need_rbf=True)
 		return self._user_txsend('alice', need_rbf=True)

+ 30 - 7
test/cmdtest_d/ct_autosign.py

@@ -81,6 +81,7 @@ class CmdTestAutosignBase(CmdTestBase):
 			atexit.register(self._macOS_eject_disk, self.asi.dev_label)
 			atexit.register(self._macOS_eject_disk, self.asi.dev_label)
 
 
 		self.opts = ['--coins='+','.join(self.coins)]
 		self.opts = ['--coins='+','.join(self.coins)]
+		self.txhex_file = f'{self.tmpdir}/tx_dump.hex'
 
 
 		if not self.live:
 		if not self.live:
 			self.spawn_env['MMGEN_TEST_SUITE_ROOT_PFX'] = self.tmpdir
 			self.spawn_env['MMGEN_TEST_SUITE_ROOT_PFX'] = self.tmpdir
@@ -228,7 +229,7 @@ class CmdTestAutosignBase(CmdTestBase):
 				t.expect('OK? (Y/n): ', '\n')
 				t.expect('OK? (Y/n): ', '\n')
 			from mmgen.mn_entry import mn_entry
 			from mmgen.mn_entry import mn_entry
 			entry_mode = 'full'
 			entry_mode = 'full'
-			mne = mn_entry(cfg, mn_type, entry_mode)
+			mne = mn_entry(cfg, mn_type, entry_mode=entry_mode)
 			if usr_entry_modes:
 			if usr_entry_modes:
 				t.expect('user-configured')
 				t.expect('user-configured')
 			else:
 			else:
@@ -492,7 +493,15 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase):
 
 
 		return do_return()
 		return do_return()
 
 
-	def _user_txsend(self, user, comment=None, no_wait=False, need_rbf=False):
+	def _user_txsend(
+			self,
+			user,
+			*,
+			comment   = None,
+			no_wait   = False,
+			need_rbf  = False,
+			dump_hex  = False,
+			mark_sent = False):
 
 
 		if need_rbf and not self.proto.cap('rbf'):
 		if need_rbf and not self.proto.cap('rbf'):
 			return 'skip'
 			return 'skip'
@@ -500,12 +509,26 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase):
 		if not no_wait:
 		if not no_wait:
 			self._wait_signed('transaction')
 			self._wait_signed('transaction')
 
 
+		extra_opt = (
+			[f'--dump-hex={self.txhex_file}'] if dump_hex
+			else ['--mark-sent'] if mark_sent
+			else [])
+
 		self.insert_device_online()
 		self.insert_device_online()
-		t = self.spawn('mmgen-txsend', [f'--{user}', '--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 = self.spawn('mmgen-txsend', [f'--{user}', '--quiet', '--autosign'] + extra_opt)
+
+		if mark_sent:
+			t.written_to_file('Sent automount transaction')
+		else:
+			t.view_tx('t')
+			t.do_comment(comment)
+			if dump_hex:
+				t.written_to_file('Serialized transaction hex data')
+				t.expect('(y/N): ', 'n') # mark as sent?
+			else:
+				self._do_confirm_send(t, quiet=True)
+				t.written_to_file('Sent automount transaction')
+
 		t.read()
 		t.read()
 		self.remove_device_online()
 		self.remove_device_online()
 		return t
 		return t

+ 3 - 2
test/cmdtest_d/ct_base.py

@@ -76,8 +76,9 @@ class CmdTestBase:
 	def get_file_with_ext(self, ext, **kwargs):
 	def get_file_with_ext(self, ext, **kwargs):
 		return get_file_with_ext(self.tmpdir, ext, **kwargs)
 		return get_file_with_ext(self.tmpdir, ext, **kwargs)
 
 
-	def read_from_tmpfile(self, fn, binary=False):
-		return read_from_file(os.path.join(self.tmpdir, fn), binary=binary)
+	def read_from_tmpfile(self, fn, binary=False, subdir=None):
+		tdir = os.path.join(self.tmpdir, subdir) if subdir else self.tmpdir
+		return read_from_file(os.path.join(tdir, fn), binary=binary)
 
 
 	def write_to_tmpfile(self, fn, data, binary=False):
 	def write_to_tmpfile(self, fn, data, binary=False):
 		return write_to_file(os.path.join(self.tmpdir, fn), data, binary=binary)
 		return write_to_file(os.path.join(self.tmpdir, fn), data, binary=binary)

+ 21 - 3
test/cmdtest_d/ct_ethdev.py

@@ -58,6 +58,7 @@ from .common import (
 )
 )
 from .ct_base import CmdTestBase
 from .ct_base import CmdTestBase
 from .ct_shared import CmdTestShared
 from .ct_shared import CmdTestShared
+from .etherscan import run_etherscan_server
 
 
 del_addrs = ('4', '1')
 del_addrs = ('4', '1')
 dfl_sid = '98831F3A'
 dfl_sid = '98831F3A'
@@ -178,6 +179,12 @@ token_bals_getbalance = lambda k: {
 
 
 coin = cfg.coin
 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):
 class CmdTestEthdev(CmdTestBase, CmdTestShared):
 	'Ethereum transacting, token deployment and tracking wallet operations'
 	'Ethereum transacting, token deployment and tracking wallet operations'
 	networks = ('eth', 'etc')
 	networks = ('eth', 'etc')
@@ -237,6 +244,8 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		('txview1_sig',          'viewing the signed transaction'),
 		('txview1_sig',          'viewing the signed transaction'),
 		('tx_status0_bad',       'getting the transaction status'),
 		('tx_status0_bad',       'getting the transaction status'),
 		('txsign1_ni',           'signing the transaction (non-interactive)'),
 		('txsign1_ni',           'signing the transaction (non-interactive)'),
+		('txsend_etherscan_test','sending the transaction via Etherscan (simulation, with --test)'),
+		('txsend_etherscan',     'sending the transaction via Etherscan (simulation)'),
 		('txsend1',              'sending the transaction'),
 		('txsend1',              'sending the transaction'),
 		('bal1',                 f'the {coin} balance'),
 		('bal1',                 f'the {coin} balance'),
 
 
@@ -426,7 +435,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		self.proto = init_proto( cfg, cfg.coin, network='regtest', need_amt=True)
 		self.proto = init_proto( cfg, cfg.coin, network='regtest', need_amt=True)
 
 
 		from mmgen.daemon import CoinDaemon
 		from mmgen.daemon import CoinDaemon
-		self.daemon = CoinDaemon( cfg, self.proto.coin+'_rt', test_suite=True)
+		self.daemon = CoinDaemon( cfg, network_id=self.proto.coin+'_rt', test_suite=True)
 
 
 		if self.daemon.id == 'reth':
 		if self.daemon.id == 'reth':
 			global dfl_devkey, dfl_devaddr
 			global dfl_devkey, dfl_devaddr
@@ -450,6 +459,9 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		self.message = 'attack at dawn'
 		self.message = 'attack at dawn'
 		self.spawn_env['MMGEN_BOGUS_SEND'] = ''
 		self.spawn_env['MMGEN_BOGUS_SEND'] = ''
 
 
+		if type(self) is CmdTestEthdev:
+			etherscan_server_start() # TODO: stop server when test group finishes executing
+
 	@property
 	@property
 	async def rpc(self):
 	async def rpc(self):
 		from mmgen.rpc import rpc_init
 		from mmgen.rpc import rpc_init
@@ -715,7 +727,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 			+ [txfile, dfl_words_file])
 			+ [txfile, dfl_words_file])
 		return self.txsign_ui_common(t, ni=ni, has_label=True)
 		return self.txsign_ui_common(t, ni=ni, has_label=True)
 
 
-	def txsend(self, ext='{}.regtest.sigtx', add_args=[]):
+	def txsend(self, ext='{}.regtest.sigtx', add_args=[], test=False):
 		ext = ext.format('-α' if cfg.debug_utf8 else '')
 		ext = ext.format('-α' if cfg.debug_utf8 else '')
 		txfile = self.get_file_with_ext(ext, no_dot=True)
 		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])
@@ -723,6 +735,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 			t,
 			t,
 			quiet      = not cfg.debug,
 			quiet      = not cfg.debug,
 			bogus_send = False,
 			bogus_send = False,
+			test       = test,
 			has_label  = True)
 			has_label  = True)
 		return t
 		return t
 
 
@@ -765,6 +778,10 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		return self.tx_status(ext='{}.regtest.sigtx', expect_str='neither in mempool nor blockchain', exit_val=1)
 		return self.tx_status(ext='{}.regtest.sigtx', expect_str='neither in mempool nor blockchain', exit_val=1)
 	def txsign1_ni(self):
 	def txsign1_ni(self):
 		return self.txsign(ni=True, dev_send=True)
 		return self.txsign(ni=True, dev_send=True)
+	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 txsend1(self):
 	def txsend1(self):
 		return self.txsend()
 		return self.txsend()
 	def txview1_sig(self): # do after send so that TxID is displayed
 	def txview1_sig(self): # do after send so that TxID is displayed
@@ -1127,7 +1144,8 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 					usr_addrs[i]))
 					usr_addrs[i]))
 
 
 		def gen_addr(addr):
 		def gen_addr(addr):
-			return tool_cmd(cfg, cmdname='gen_addr', proto=self.proto).gen_addr(addr, dfl_words_file)
+			return tool_cmd(
+				cfg, cmdname='gen_addr', proto=self.proto).gen_addr(addr, wallet=dfl_words_file)
 
 
 		silence()
 		silence()
 		usr_addrs = list(map(gen_addr, usr_mmaddrs))
 		usr_addrs = list(map(gen_addr, usr_mmaddrs))

+ 3 - 3
test/cmdtest_d/ct_input.py

@@ -422,7 +422,7 @@ class CmdTestInput(CmdTestBase):
 		mn = mn or sample_mn[fmt]['mn'].split()
 		mn = mn or sample_mn[fmt]['mn'].split()
 		t = self.spawn('mmgen-tool', ['mn2hex_interactive', 'fmt='+fmt, 'mn_len=12', 'print_mn=1'])
 		t = self.spawn('mmgen-tool', ['mn2hex_interactive', 'fmt='+fmt, 'mn_len=12', 'print_mn=1'])
 		from mmgen.mn_entry import mn_entry
 		from mmgen.mn_entry import mn_entry
-		mne = mn_entry(cfg, fmt, entry_mode)
+		mne = mn_entry(cfg, fmt, entry_mode=entry_mode)
 		t.expect(
 		t.expect(
 			'Type a number.*: ',
 			'Type a number.*: ',
 			('\n' if enter_for_dfl else str(mne.entry_modes.index(entry_mode)+1)),
 			('\n' if enter_for_dfl else str(mne.entry_modes.index(entry_mode)+1)),
@@ -465,7 +465,7 @@ class CmdTestInput(CmdTestBase):
 			t.expect('Type a number.*: ', '6', regex=True)
 			t.expect('Type a number.*: ', '6', regex=True)
 			t.expect('invalid')
 			t.expect('invalid')
 			from mmgen.mn_entry import mn_entry
 			from mmgen.mn_entry import mn_entry
-			mne = mn_entry(cfg, fmt, entry_mode)
+			mne = mn_entry(cfg, fmt, entry_mode=entry_mode)
 			t.expect('Type a number.*: ', str(mne.entry_modes.index(entry_mode)+1), regex=True)
 			t.expect('Type a number.*: ', str(mne.entry_modes.index(entry_mode)+1), regex=True)
 			t.expect(r'Using entry mode (\S+)', regex=True)
 			t.expect(r'Using entry mode (\S+)', regex=True)
 			mode = strip_ansi_escapes(t.p.match.group(1)).lower()
 			mode = strip_ansi_escapes(t.p.match.group(1)).lower()
@@ -489,7 +489,7 @@ class CmdTestInput(CmdTestBase):
 	def mnemonic_entry_mmgen_minimal(self):
 	def mnemonic_entry_mmgen_minimal(self):
 		from mmgen.mn_entry import mn_entry
 		from mmgen.mn_entry import mn_entry
 		# erase_chars: '\b\x7f'
 		# erase_chars: '\b\x7f'
-		m = mn_entry(cfg, 'mmgen', 'minimal')
+		m = mn_entry(cfg, 'mmgen', entry_mode='minimal')
 		np = 2
 		np = 2
 		mn = (
 		mn = (
 			'z',
 			'z',

+ 11 - 5
test/cmdtest_d/ct_main.py

@@ -68,7 +68,7 @@ def make_brainwallet_file(fn):
 	d = ''.join(rand_pairs).rstrip() + '\n'
 	d = ''.join(rand_pairs).rstrip() + '\n'
 	if cfg.verbose:
 	if cfg.verbose:
 		msg_r(f'Brainwallet password:\n{cyan(d)}')
 		msg_r(f'Brainwallet password:\n{cyan(d)}')
-	write_data_to_file(cfg, fn, d, 'brainwallet password', quiet=True, ignore_opt_outdir=True)
+	write_data_to_file(cfg, fn, d, desc='brainwallet password', quiet=True, ignore_opt_outdir=True)
 
 
 def verify_checksum_or_exit(checksum, chk):
 def verify_checksum_or_exit(checksum, chk):
 	chk = strip_ansi_escapes(chk)
 	chk = strip_ansi_escapes(chk)
@@ -397,7 +397,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		addrfile = self.get_file_with_ext('addrs')
 		addrfile = self.get_file_with_ext('addrs')
 		from mmgen.addrlist import AddrList
 		from mmgen.addrlist import AddrList
 		silence()
 		silence()
-		chk = AddrList(cfg, self.proto, addrfile).chksum
+		chk = AddrList(cfg, self.proto, infile=addrfile).chksum
 		end_silence()
 		end_silence()
 		if cfg.verbose and display:
 		if cfg.verbose and display:
 			msg(f'Checksum: {cyan(chk)}')
 			msg(f'Checksum: {cyan(chk)}')
@@ -533,7 +533,13 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		return self.walletchk(wf, wcls=wcls, dfl_wallet=dfl_wallet)
 		return self.walletchk(wf, wcls=wcls, dfl_wallet=dfl_wallet)
 
 
 	def _write_fake_data_to_file(self, d):
 	def _write_fake_data_to_file(self, d):
-		write_data_to_file(cfg, self.unspent_data_file, d, 'Unspent outputs', quiet=True, ignore_opt_outdir=True)
+		write_data_to_file(
+				cfg,
+				self.unspent_data_file,
+				d,
+				desc              = 'Unspent outputs',
+				quiet             = True,
+				ignore_opt_outdir = True)
 		if cfg.verbose or cfg.exact_output:
 		if cfg.verbose or cfg.exact_output:
 			sys.stderr.write(f'Fake transaction wallet data written to file {self.unspent_data_file!r}\n')
 			sys.stderr.write(f'Fake transaction wallet data written to file {self.unspent_data_file!r}\n')
 
 
@@ -627,7 +633,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		tx_data, ad = {}, AddrData(self.proto)
 		tx_data, ad = {}, AddrData(self.proto)
 		for s in sources:
 		for s in sources:
 			addrfile = get_file_with_ext(self.cfgs[s]['tmpdir'], 'addrs')
 			addrfile = get_file_with_ext(self.cfgs[s]['tmpdir'], 'addrs')
-			al = AddrList(cfg, self.proto, addrfile)
+			al = AddrList(cfg, self.proto, infile=addrfile)
 			ad.add(al)
 			ad.add(al)
 			aix = AddrIdxList(fmt_str=self.cfgs[s]['addr_idx_list'])
 			aix = AddrIdxList(fmt_str=self.cfgs[s]['addr_idx_list'])
 			if len(aix) != addrs_per_wallet:
 			if len(aix) != addrs_per_wallet:
@@ -843,7 +849,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		wcls = get_wallet_cls(fmt_code=out_fmt)
 		wcls = get_wallet_cls(fmt_code=out_fmt)
 		msg('==> {}: {}'.format(
 		msg('==> {}: {}'.format(
 			wcls.desc,
 			wcls.desc,
-			cyan(get_data_from_file(cfg, f, wcls.desc))
+			cyan(get_data_from_file(cfg, f, desc=wcls.desc))
 		))
 		))
 		end_silence()
 		end_silence()
 		return t
 		return t

+ 97 - 21
test/cmdtest_d/ct_regtest.py

@@ -27,7 +27,7 @@ from mmgen.proto.btc.regtest import MMGenRegtest
 from mmgen.proto.bch.cashaddr import b32a
 from mmgen.proto.bch.cashaddr import b32a
 from mmgen.proto.btc.common import b58a
 from mmgen.proto.btc.common import b58a
 from mmgen.color import yellow
 from mmgen.color import yellow
-from mmgen.util import msg_r, die, gmsg, capfirst, suf, fmt_list
+from mmgen.util import msg_r, die, gmsg, capfirst, suf, fmt_list, is_hex_str
 from mmgen.protocol import init_proto
 from mmgen.protocol import init_proto
 from mmgen.addrlist import AddrList
 from mmgen.addrlist import AddrList
 from mmgen.wallet import Wallet, get_wallet_cls
 from mmgen.wallet import Wallet, get_wallet_cls
@@ -189,14 +189,15 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		('subgroup.view',           ['label']),
 		('subgroup.view',           ['label']),
 		('subgroup._auto_chg_deps', ['twexport', 'label']),
 		('subgroup._auto_chg_deps', ['twexport', 'label']),
 		('subgroup.auto_chg',       ['_auto_chg_deps']),
 		('subgroup.auto_chg',       ['_auto_chg_deps']),
+		('subgroup.dump_hex',       ['fund_users']),
 		('stop',                    'stopping regtest daemon'),
 		('stop',                    'stopping regtest daemon'),
 	)
 	)
 	cmd_subgroups = {
 	cmd_subgroups = {
 	'misc': (
 	'misc': (
 		'miscellaneous commands',
 		'miscellaneous commands',
-		('daemon_version',         'mmgen-tool daemon_version'),
-		('halving_calculator_bob', 'halving calculator (Bob)'),
-		('cli_txcreate',           '‘mmgen-cli createrawtransaction’'),
+		('daemon_version',           'mmgen-tool daemon_version'),
+		('halving_calculator_bob',   'halving calculator (Bob)'),
+		('cli_createrawtransaction', '‘mmgen-cli createrawtransaction’'),
 	),
 	),
 	'init_bob': (
 	'init_bob': (
 		'creating Bob’s MMGen wallet and tracking wallet',
 		'creating Bob’s MMGen wallet and tracking wallet',
@@ -224,7 +225,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		('fund_bob',                     'funding Bob’s wallet'),
 		('fund_bob',                     'funding Bob’s wallet'),
 		('fund_alice',                   'funding Alice’s wallet'),
 		('fund_alice',                   'funding Alice’s wallet'),
 		('generate',                     'mining a block'),
 		('generate',                     'mining a block'),
-		('bob_bal1_cli',                 'Bob’s balance (via ‘mmgen-cli’)'),
+		('bob_bal1',                     'Bob’s balance'),
 		('generate_extra_deterministic', 'generate extra blocks for deterministic run'),
 		('generate_extra_deterministic', 'generate extra blocks for deterministic run'),
 	),
 	),
 	'msg': (
 	'msg': (
@@ -458,6 +459,17 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 									'(no unused addresses)'),
 									'(no unused addresses)'),
 		('carol_delete_wallet',      'unloading and deleting Carol’s tracking wallet'),
 		('carol_delete_wallet',      'unloading and deleting Carol’s tracking wallet'),
 	),
 	),
+	'dump_hex': (
+		'sending from dumped hex',
+		('bob_dump_hex_create',      'dump_hex transaction - creating'),
+		('bob_dump_hex_sign',        'dump_hex transaction - signing'),
+		('bob_dump_hex_dump_stdout', 'dump_hex transaction - dumping tx hex to stdout'),
+		('bob_dump_hex_dump',        'dump_hex transaction - dumping tx hex to file'),
+		('bob_dump_hex_test',        'dump_hex transaction - test whether TX can be sent'),
+		('bob_dump_hex_send_cli',    'dump_hex transaction - sending via cli'),
+		('generate',                 'mining a block'),
+		('bob_bal7',                 'Bob’s balance'),
+	),
 	}
 	}
 
 
 	def __init__(self, trunner, cfgs, spawn):
 	def __init__(self, trunner, cfgs, spawn):
@@ -494,11 +506,12 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		self.burn_addr = make_burn_addr(self.proto)
 		self.burn_addr = make_burn_addr(self.proto)
 		self.user_sids = {}
 		self.user_sids = {}
 		self.protos = (self.proto,)
 		self.protos = (self.proto,)
+		self.dump_hex_subdir = os.path.join(self.tmpdir, 'nochg_tx')
 
 
 	def _add_comments_to_addr_file(self, proto, addrfile, outfile, use_comments=False):
 	def _add_comments_to_addr_file(self, proto, addrfile, outfile, use_comments=False):
 		silence()
 		silence()
 		gmsg(f'Adding comments to address file {addrfile!r}')
 		gmsg(f'Adding comments to address file {addrfile!r}')
-		a = AddrList(cfg, proto, addrfile)
+		a = AddrList(cfg, proto, infile=addrfile)
 		for n, idx in enumerate(a.idxs(), 1):
 		for n, idx in enumerate(a.idxs(), 1):
 			if use_comments:
 			if use_comments:
 				a.set_comment(idx, get_comment())
 				a.set_comment(idx, get_comment())
@@ -548,7 +561,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		t.expect('time until halving')
 		t.expect('time until halving')
 		return t
 		return t
 
 
-	def cli_txcreate(self):
+	def cli_createrawtransaction(self):
 		txid = 'beadcafe' * 8
 		txid = 'beadcafe' * 8
 		return self.spawn(
 		return self.spawn(
 			'mmgen-cli',
 			'mmgen-cli',
@@ -777,6 +790,15 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 	def bob_twview1(self):
 	def bob_twview1(self):
 		return self.user_twview('bob', chk=('1', rtAmts[0]))
 		return self.user_twview('bob', chk=('1', rtAmts[0]))
 
 
+	def _user_bal_cli(self, user, *, chk=None, chks=[]):
+		t = self.spawn('mmgen-cli', [f'--{user}', 'getbalance', '*', '1', 'true'])
+		res = t.read().splitlines()[0].rstrip('0').rstrip('.')
+		if chk:
+			assert res == chk, f'{res}: invalid balance! (expected {chk})'
+		else:
+			assert res in chks, f'{res}: invalid balance! (expected one of {chks})'
+		return t
+
 	def user_bal(self, user, bal, opts=[], args=['showempty=1'], skip_check=False, proto=None):
 	def user_bal(self, user, bal, opts=[], args=['showempty=1'], skip_check=False, proto=None):
 		proto = proto or self.proto
 		proto = proto or self.proto
 		t = self.spawn('mmgen-tool', opts + [f'--{user}', f'--coin={proto.coin}', 'listaddresses'] + args)
 		t = self.spawn('mmgen-tool', opts + [f'--{user}', f'--coin={proto.coin}', 'listaddresses'] + args)
@@ -785,20 +807,16 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		return t
 		return t
 
 
 	def alice_bal1(self):
 	def alice_bal1(self):
-		return self.user_bal('alice', rtFundAmt)
+		return self._user_bal_cli('alice', chk=rtFundAmt)
 
 
 	def alice_bal2(self):
 	def alice_bal2(self):
-		return self.user_bal('alice', rtBals[8])
+		return self._user_bal_cli('alice', chk=rtBals[8])
 
 
-	def bob_bal1_cli(self):
-		t = self.spawn(
-			'mmgen-cli',
-			['--regtest=1', '--wallet=bob', f'--coin={self.proto.coin}', 'getbalance', '*', '0', 'true'])
-		t.expect(rtFundAmt + '.00')
-		return t
+	def bob_bal1(self):
+		return self._user_bal_cli('bob', chk=rtFundAmt)
 
 
 	def bob_bal2(self):
 	def bob_bal2(self):
-		return self.user_bal('bob', rtBals[0], self._cashaddr_opt(1))
+		return self._user_bal_cli('bob', chk=rtBals[0])
 
 
 	def bob_bal2a(self):
 	def bob_bal2a(self):
 		return self.user_bal('bob', rtBals[0], args=['showempty=1', 'age_fmt=confs'])
 		return self.user_bal('bob', rtBals[0], args=['showempty=1', 'age_fmt=confs'])
@@ -819,16 +837,16 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		return self.user_bal('bob', rtBals[0], args=['showempty=0', 'sort=twmmid', 'reverse=1'])
 		return self.user_bal('bob', rtBals[0], args=['showempty=0', 'sort=twmmid', 'reverse=1'])
 
 
 	def bob_bal3(self):
 	def bob_bal3(self):
-		return self.user_bal('bob', rtBals[1])
+		return self._user_bal_cli('bob', chk=rtBals[1])
 
 
 	def bob_bal4(self):
 	def bob_bal4(self):
-		return self.user_bal('bob', rtBals[2])
+		return self._user_bal_cli('bob', chk=rtBals[2])
 
 
 	def bob_bal5(self):
 	def bob_bal5(self):
-		return self.user_bal('bob', rtBals[3])
+		return self._user_bal_cli('bob', chk=rtBals[3])
 
 
 	def bob_bal6(self):
 	def bob_bal6(self):
-		return self.user_bal('bob', rtBals[7])
+		return self._user_bal_cli('bob', chk=rtBals[7])
 
 
 	def bob_subwallet_addrgen1(self):
 	def bob_subwallet_addrgen1(self):
 		return self.addrgen('bob', subseed_idx='29L', mmtypes=['C'])  # 29L: 2FA7BBA8
 		return self.addrgen('bob', subseed_idx='29L', mmtypes=['C'])  # 29L: 2FA7BBA8
@@ -1110,7 +1128,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 			sid, self.get_altcoin_pfx(proto.coin), id_str, addr_range, x='-α' if cfg.debug_utf8 else '')
 			sid, self.get_altcoin_pfx(proto.coin), id_str, addr_range, x='-α' if cfg.debug_utf8 else '')
 		addrfile = get_file_with_ext(self._user_dir(user), ext, no_dot=True)
 		addrfile = get_file_with_ext(self._user_dir(user), ext, no_dot=True)
 		silence()
 		silence()
-		addr = AddrList(cfg, proto, addrfile).data[idx].addr
+		addr = AddrList(cfg, proto, infile=addrfile).data[idx].addr
 		end_silence()
 		end_silence()
 		return addr
 		return addr
 
 
@@ -2180,6 +2198,64 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 			'L',
 			'L',
 			'contains no unused addresses of address type')
 			'contains no unused addresses of address type')
 
 
+	def bob_dump_hex_create(self):
+		if not os.path.exists(self.dump_hex_subdir):
+			os.mkdir(self.dump_hex_subdir)
+		autochg_arg = self._user_sid('bob') + ':C'
+		return self.txcreate_ui_common(
+			self.spawn('mmgen-txcreate',
+				[
+					'-d',
+					self.dump_hex_subdir,
+					'-B',
+					'--bob',
+					'--fee=0.00009713',
+					autochg_arg
+				]),
+			auto_chg_addr = autochg_arg)
+
+	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]),
+			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.expect('view: ', '\n')
+		t.expect('(y/N): ', '\n') # add comment?
+		t.written_to_file('Sent transaction')
+		return t
+
+	def bob_dump_hex_dump(self):
+		return self._bob_dump_hex_dump('tx_dump.hex')
+
+	def bob_dump_hex_dump_stdout(self):
+		return self._bob_dump_hex_dump('-')
+
+	def _user_dump_hex_send_cli(self, user, *, subdir=None):
+		txhex = self.read_from_tmpfile('tx_dump.hex', subdir=subdir).strip()
+		t = self.spawn('mmgen-cli', [f'--{user}', 'sendrawtransaction', txhex])
+		txid = t.read().splitlines()[0]
+		assert is_hex_str(txid) and len(txid) == 64
+		return t
+
+	def bob_dump_hex_test(self):
+		txfile = get_file_with_ext(self.dump_hex_subdir, 'sigtx')
+		t = self.spawn('mmgen-txsend', ['--bob', '--test', txfile])
+		self.txsend_ui_common(t, bogus_send=False, test=True)
+		return t
+
+	def bob_dump_hex_send_cli(self):
+		return self._user_dump_hex_send_cli('bob', subdir='nochg_tx')
+
+	def bob_bal7(self):
+		if not self.coin == 'btc':
+			return 'skip'
+		return self._user_bal_cli('bob', chks=['499.99990287', '46.51845565'])
+
 	def stop(self):
 	def stop(self):
 		self.spawn('', msg_only=True)
 		self.spawn('', msg_only=True)
 		if cfg.no_daemon_stop:
 		if cfg.no_daemon_stop:

+ 17 - 7
test/cmdtest_d/ct_shared.py

@@ -134,15 +134,18 @@ class CmdTestShared:
 			ni          = False,
 			ni          = False,
 			save        = True,
 			save        = True,
 			do_passwd   = False,
 			do_passwd   = False,
+			passwd      = None,
 			has_label   = False):
 			has_label   = False):
 
 
 		txdo = (caller or self.test_name)[:4] == 'txdo'
 		txdo = (caller or self.test_name)[:4] == 'txdo'
 
 
-		if do_passwd:
-			t.passphrase('MMGen wallet', self.wpasswd)
+		if do_passwd and txdo:
+			t.passphrase('MMGen wallet', passwd or self.wpasswd)
 
 
-		if not ni and not txdo:
+		if not (ni or txdo):
 			t.view_tx(view)
 			t.view_tx(view)
+			if do_passwd:
+				t.passphrase('MMGen wallet', passwd or self.wpasswd)
 			t.do_comment(add_comment, has_label=has_label)
 			t.do_comment(add_comment, has_label=has_label)
 			t.expect('(Y/n): ', ('n', 'y')[save])
 			t.expect('(Y/n): ', ('n', 'y')[save])
 
 
@@ -159,6 +162,7 @@ class CmdTestShared:
 			file_desc    = 'Sent transaction',
 			file_desc    = 'Sent transaction',
 			confirm_send = True,
 			confirm_send = True,
 			bogus_send   = True,
 			bogus_send   = True,
+			test         = False,
 			quiet        = False,
 			quiet        = False,
 			has_label    = False):
 			has_label    = False):
 
 
@@ -169,16 +173,22 @@ class CmdTestShared:
 			t.view_tx(view)
 			t.view_tx(view)
 			t.do_comment(add_comment, has_label=has_label)
 			t.do_comment(add_comment, has_label=has_label)
 
 
-		self._do_confirm_send(t, quiet=quiet, confirm_send=confirm_send)
+		if not test:
+			self._do_confirm_send(t, quiet=quiet, confirm_send=confirm_send)
 
 
 		if bogus_send:
 		if bogus_send:
 			txid = ''
 			txid = ''
 			t.expect('BOGUS transaction NOT sent')
 			t.expect('BOGUS transaction NOT sent')
+		elif test == 'tx_proxy':
+			t.expect('can be sent')
+			return True
 		else:
 		else:
-			txid = strip_ansi_escapes(t.expect_getend('Transaction sent: '))
+			m = 'TxID: ' if test else 'Transaction sent: '
+			txid = strip_ansi_escapes(t.expect_getend(m))
 			assert len(txid) == 64, f'{txid!r}: Incorrect txid length!'
 			assert len(txid) == 64, f'{txid!r}: Incorrect txid length!'
 
 
-		t.written_to_file(file_desc)
+		if not test:
+			t.written_to_file(file_desc)
 
 
 		return txid
 		return txid
 
 
@@ -295,7 +305,7 @@ class CmdTestShared:
 			fn = t.written_to_file('Password list' if passgen else 'Addresses')
 			fn = t.written_to_file('Password list' if passgen else 'Addresses')
 			cls = PasswordList if passgen else AddrList
 			cls = PasswordList if passgen else AddrList
 			silence()
 			silence()
-			al = cls(cfg, self.proto, fn, skip_chksum_msg=True) # read back the file we’ve written
+			al = cls(cfg, self.proto, infile=fn, skip_chksum_msg=True) # read back the file we’ve written
 			end_silence()
 			end_silence()
 			cmp_or_die(al.chksum, chksum, desc=f'{ftype}list data checksum from file')
 			cmp_or_die(al.chksum, chksum, desc=f'{ftype}list data checksum from file')
 		return t
 		return t

+ 1 - 1
test/cmdtest_d/ct_swap.py

@@ -669,7 +669,7 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 		data['chksum'] = make_chksum_6(json_dumps(data['MMGenTransaction']))
 		data['chksum'] = make_chksum_6(json_dumps(data['MMGenTransaction']))
 		with open(fn, 'w') as fh:
 		with open(fn, 'w') as fh:
 			json.dump(data, fh)
 			json.dump(data, fh)
-		t = self.spawn('mmgen-txsend', ['-d', self.tmpdir, '--bob', fn], exit_val=2)
+		t = self.spawn('mmgen-txsend', ['-d', self.tmpdir, '--bob', fn], exit_val=1)
 		t.expect('expired')
 		t.expect('expired')
 		return t
 		return t
 
 

+ 1 - 1
test/cmdtest_d/ct_xmr_autosign.py

@@ -133,7 +133,7 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 			cfg       = self.cfg,
 			cfg       = self.cfg,
 			proto     = self.proto,
 			proto     = self.proto,
 			addr_idxs = '1-2',
 			addr_idxs = '1-2',
-			seed      = Wallet(cfg, data.mmwords).seed,
+			seed      = Wallet(cfg, fn=data.mmwords).seed,
 			skip_chksum_msg = True,
 			skip_chksum_msg = True,
 			key_address_validity_check = False)
 			key_address_validity_check = False)
 		kal.file.write(ask_overwrite=False)
 		kal.file.write(ask_overwrite=False)

+ 3 - 11
test/cmdtest_d/ct_xmrwallet.py

@@ -25,6 +25,7 @@ from subprocess import run, PIPE
 from collections import namedtuple
 from collections import namedtuple
 
 
 from mmgen.util import msg, fmt, async_run, capfirst, is_int, die, list_gen
 from mmgen.util import msg, fmt, async_run, capfirst, is_int, die, list_gen
+from mmgen.util2 import port_in_use
 from mmgen.obj import MMGenRange
 from mmgen.obj import MMGenRange
 from mmgen.amt import XMRAmt
 from mmgen.amt import XMRAmt
 from mmgen.addrlist import ViewKeyAddrList, KeyAddrList, AddrIdxList
 from mmgen.addrlist import ViewKeyAddrList, KeyAddrList, AddrIdxList
@@ -155,15 +156,6 @@ class CmdTestXMRWallet(CmdTestBase):
 	@classmethod
 	@classmethod
 	def init_proxy(cls, external_call=False):
 	def init_proxy(cls, external_call=False):
 
 
-		def port_in_use(port):
-			import socket
-			try:
-				socket.create_connection(('localhost', port)).close()
-			except:
-				return False
-			else:
-				return True
-
 		def start_proxy():
 		def start_proxy():
 			if external_call or not cfg.no_daemon_autostart:
 			if external_call or not cfg.no_daemon_autostart:
 				run(a+b2)
 				run(a+b2)
@@ -512,7 +504,7 @@ class CmdTestXMRWallet(CmdTestBase):
 			+ ([] if data.autosign else [data.kafile])
 			+ ([] if data.autosign else [data.kafile])
 			+ ([wallets] if wallets else [])
 			+ ([wallets] if wallets else [])
 		)
 		)
-		wlist = AddrIdxList(wallets) if wallets else MMGenRange(data.kal_range).items
+		wlist = AddrIdxList(fmt_str=wallets) if wallets else MMGenRange(data.kal_range).items
 		for n, wnum in enumerate(wlist, 1):
 		for n, wnum in enumerate(wlist, 1):
 			t.expect('ing wallet {}/{} ({})'.format(
 			t.expect('ing wallet {}/{} ({})'.format(
 				n,
 				n,
@@ -684,7 +676,7 @@ class CmdTestXMRWallet(CmdTestBase):
 		kal = (ViewKeyAddrList if data.autosign else KeyAddrList)(
 		kal = (ViewKeyAddrList if data.autosign else KeyAddrList)(
 			cfg      = cfg,
 			cfg      = cfg,
 			proto    = self.proto,
 			proto    = self.proto,
-			addrfile = data.kafile,
+			infile   = data.kafile,
 			skip_chksum_msg = True,
 			skip_chksum_msg = True,
 			key_address_validity_check = False)
 			key_address_validity_check = False)
 		end_silence()
 		end_silence()

+ 32 - 0
test/cmdtest_d/etherscan.py

@@ -0,0 +1,32 @@
+#!/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')

+ 2 - 2
test/daemontest_d/ut_exec.py

@@ -14,7 +14,7 @@ from mmgen.daemon import CoinDaemon
 from ..include.common import cfg, qmsg, qmsg_r, vmsg, msg
 from ..include.common import cfg, qmsg, qmsg_r, vmsg, msg
 
 
 def test_flags(coin):
 def test_flags(coin):
-	d = CoinDaemon(cfg, coin)
+	d = CoinDaemon(cfg, network_id=coin)
 	vmsg(f'Available opts:  {fmt_list(d.avail_opts, fmt="bare")}')
 	vmsg(f'Available opts:  {fmt_list(d.avail_opts, fmt="bare")}')
 	vmsg(f'Available flags: {fmt_list(d.avail_flags, fmt="bare")}')
 	vmsg(f'Available flags: {fmt_list(d.avail_flags, fmt="bare")}')
 	vals = namedtuple('vals', ['online', 'no_daemonize', 'keep_cfg_file'])
 	vals = namedtuple('vals', ['online', 'no_daemonize', 'keep_cfg_file'])
@@ -26,7 +26,7 @@ def test_flags(coin):
 				(['online'],                 ['keep_cfg_file'], vals(True, False, True)),
 				(['online'],                 ['keep_cfg_file'], vals(True, False, True)),
 				(['online', 'no_daemonize'], ['keep_cfg_file'], vals(True, True, True)),
 				(['online', 'no_daemonize'], ['keep_cfg_file'], vals(True, True, True)),
 			):
 			):
-			d = CoinDaemon(cfg, coin, opts=opts, flags=flags)
+			d = CoinDaemon(cfg, network_id=coin, opts=opts, flags=flags)
 			assert d.flag.keep_cfg_file == val.keep_cfg_file
 			assert d.flag.keep_cfg_file == val.keep_cfg_file
 			assert d.opt.online == val.online
 			assert d.opt.online == val.online
 			assert d.opt.no_daemonize == val.no_daemonize
 			assert d.opt.no_daemonize == val.no_daemonize

+ 3 - 3
test/daemontest_d/ut_msg.py

@@ -84,13 +84,13 @@ async def run_test(network_id, chksum, msghash_type='raw'):
 	print_total(await m.verify())
 	print_total(await m.verify())
 
 
 	pumsg('\nTesting single address verification:\n')
 	pumsg('\nTesting single address verification:\n')
-	print_total(await m.verify(single_addr))
+	print_total(await m.verify(addr=single_addr))
 
 
 	pumsg('\nTesting JSON dump for export:\n')
 	pumsg('\nTesting JSON dump for export:\n')
 	msg(m.get_json_for_export())
 	msg(m.get_json_for_export())
 
 
 	pumsg('\nTesting single address JSON dump for export:\n')
 	pumsg('\nTesting single address JSON dump for export:\n')
-	msg(m.get_json_for_export(single_addr))
+	msg(m.get_json_for_export(addr=single_addr))
 
 
 	from mmgen.fileutil import write_data_to_file
 	from mmgen.fileutil import write_data_to_file
 	exported_sigs = os.path.join(tmpdir, 'signatures.json')
 	exported_sigs = os.path.join(tmpdir, 'signatures.json')
@@ -107,7 +107,7 @@ async def run_test(network_id, chksum, msghash_type='raw'):
 	print_total(await m.verify())
 	print_total(await m.verify())
 
 
 	pumsg('\nTesting single address verification (exported data):\n')
 	pumsg('\nTesting single address verification (exported data):\n')
-	print_total(await m.verify(single_addr_coin))
+	print_total(await m.verify(addr=single_addr_coin))
 
 
 	pumsg('\nTesting display (exported data):\n')
 	pumsg('\nTesting display (exported data):\n')
 	msg(m.format())
 	msg(m.format())

+ 7 - 4
test/daemontest_d/ut_rpc.py

@@ -91,7 +91,7 @@ class init_test:
 
 
 	@staticmethod
 	@staticmethod
 	async def btc(cfg, daemon, backend, cfg_override):
 	async def btc(cfg, daemon, backend, cfg_override):
-		rpc = await rpc_init(cfg, daemon.proto, backend, daemon)
+		rpc = await rpc_init(cfg, daemon.proto, backend=backend, daemon=daemon)
 		do_msg(rpc, backend)
 		do_msg(rpc, backend)
 
 
 		wi = await rpc.walletinfo
 		wi = await rpc.walletinfo
@@ -106,7 +106,7 @@ class init_test:
 
 
 	@staticmethod
 	@staticmethod
 	async def bch(cfg, daemon, backend, cfg_override):
 	async def bch(cfg, daemon, backend, cfg_override):
-		rpc = await rpc_init(cfg, daemon.proto, backend, daemon)
+		rpc = await rpc_init(cfg, daemon.proto, backend=backend, daemon=daemon)
 		do_msg(rpc, backend)
 		do_msg(rpc, backend)
 		return rpc
 		return rpc
 
 
@@ -114,7 +114,7 @@ class init_test:
 
 
 	@staticmethod
 	@staticmethod
 	async def eth(cfg, daemon, backend, cfg_override):
 	async def eth(cfg, daemon, backend, cfg_override):
-		rpc = await rpc_init(cfg, daemon.proto, backend, daemon)
+		rpc = await rpc_init(cfg, daemon.proto, backend=backend, daemon=daemon)
 		do_msg(rpc, backend)
 		do_msg(rpc, backend)
 		await rpc.call('eth_blockNumber', timeout=300)
 		await rpc.call('eth_blockNumber', timeout=300)
 		if rpc.proto.network == 'testnet':
 		if rpc.proto.network == 'testnet':
@@ -167,7 +167,7 @@ async def run_test(network_ids, test_cf_auth=False, daemon_ids=None, cfg_overrid
 
 
 class unit_tests:
 class unit_tests:
 
 
-	altcoin_deps = ('ltc', 'bch', 'geth', 'erigon', 'parity', 'xmrwallet')
+	altcoin_deps = ('ltc', 'bch', 'geth', 'reth', 'erigon', 'parity', 'xmrwallet')
 	arm_skip = ('parity',) # no prebuilt binaries for ARM
 	arm_skip = ('parity',) # no prebuilt binaries for ARM
 
 
 	async def btc(self, name, ut):
 	async def btc(self, name, ut):
@@ -204,6 +204,9 @@ class unit_tests:
 				'eth_testnet_chain_names': ['goerli', 'holesky', 'foo', 'bar', 'baz'],
 				'eth_testnet_chain_names': ['goerli', 'holesky', 'foo', 'bar', 'baz'],
 		})
 		})
 
 
+	async def reth(self, name, ut):
+		return await run_test(['eth', 'eth_rt'], daemon_ids=['reth']) # TODO: eth_tn
+
 	async def erigon(self, name, ut):
 	async def erigon(self, name, ut):
 		return await run_test(['eth', 'eth_tn', 'eth_rt'], daemon_ids=['erigon'])
 		return await run_test(['eth', 'eth_tn', 'eth_rt'], daemon_ids=['erigon'])
 
 

+ 1 - 1
test/daemontest_d/ut_tx.py

@@ -110,7 +110,7 @@ class unit_tests:
 
 
 	async def newtx(self, name, ut):
 	async def newtx(self, name, ut):
 		qmsg('  Testing NewTX initializer')
 		qmsg('  Testing NewTX initializer')
-		d = CoinDaemon(cfg, 'btc', test_suite=True)
+		d = CoinDaemon(cfg, network_id='btc', test_suite=True)
 		d.start()
 		d.start()
 
 
 		proto = init_proto(cfg, 'btc', need_amt=True)
 		proto = init_proto(cfg, 'btc', need_amt=True)

+ 3 - 3
test/gentest.py

@@ -322,7 +322,7 @@ def do_ab_test(proto, scfg, addr_type, gen1, kg2, ag, tool, cache_data):
 			for _ in range(scfg.rounds):
 			for _ in range(scfg.rounds):
 				yield getrand(32)
 				yield getrand(32)
 
 
-	kg1 = KeyGenerator(cfg, proto, addr_type.pubkey_type, gen1)
+	kg1 = KeyGenerator(cfg, proto, addr_type.pubkey_type, backend=gen1)
 	if type(kg1) == type(kg2):
 	if type(kg1) == type(kg2):
 		die(4, 'Key generators are the same!')
 		die(4, 'Key generators are the same!')
 
 
@@ -378,7 +378,7 @@ def ab_test(proto, scfg):
 
 
 	if scfg.gen2:
 	if scfg.gen2:
 		assert scfg.gen1 != 'all', "'all' must be used only with external tool"
 		assert scfg.gen1 != 'all', "'all' must be used only with external tool"
-		kg2 = KeyGenerator(cfg, proto, addr_type.pubkey_type, scfg.gen2)
+		kg2 = KeyGenerator(cfg, proto, addr_type.pubkey_type, backend=scfg.gen2)
 		tool = None
 		tool = None
 	else:
 	else:
 		toolname = find_or_check_tool(proto, addr_type, scfg.tool)
 		toolname = find_or_check_tool(proto, addr_type, scfg.tool)
@@ -541,7 +541,7 @@ def main():
 		for p in protos:
 		for p in protos:
 			ab_test(p, scfg)
 			ab_test(p, scfg)
 	else:
 	else:
-		kg = KeyGenerator(cfg, proto, addr_type.pubkey_type, scfg.gen1)
+		kg = KeyGenerator(cfg, proto, addr_type.pubkey_type, backend=scfg.gen1)
 		ag = AddrGenerator(cfg, proto, addr_type)
 		ag = AddrGenerator(cfg, proto, addr_type)
 		if scfg.test == 'speed':
 		if scfg.test == 'speed':
 			speed_test(proto, kg, ag, scfg.rounds)
 			speed_test(proto, kg, ag, scfg.rounds)

+ 1 - 1
test/include/common.py

@@ -307,7 +307,7 @@ def test_daemons_ops(*network_ids, op, remove_datadir=False):
 		silent = not (cfg.verbose or cfg.exact_output)
 		silent = not (cfg.verbose or cfg.exact_output)
 		ret = False
 		ret = False
 		for network_id in network_ids:
 		for network_id in network_ids:
-			d = CoinDaemon(cfg, network_id, test_suite=True)
+			d = CoinDaemon(cfg, network_id=network_id, test_suite=True)
 			if remove_datadir:
 			if remove_datadir:
 				d.wait = True
 				d.wait = True
 				d.stop(silent=True)
 				d.stop(silent=True)

+ 2 - 2
test/modtest_d/ut_addrlist.py

@@ -27,7 +27,7 @@ def do_test(
 	proto = init_proto(cfg, coin or 'btc')
 	proto = init_proto(cfg, coin or 'btc')
 	seed = Seed(cfg, seed_bin=bytes.fromhex('feedbead'*8))
 	seed = Seed(cfg, seed_bin=bytes.fromhex('feedbead'*8))
 	mmtype = MMGenAddrType(proto, addrtype or 'C')
 	mmtype = MMGenAddrType(proto, addrtype or 'C')
-	idxs = AddrIdxList(idx_spec or '1-3')
+	idxs = AddrIdxList(fmt_str=idx_spec or '1-3')
 
 
 	if cfg.verbose:
 	if cfg.verbose:
 		debug_addrlist_save = cfg.debug_addrlist
 		debug_addrlist_save = cfg.debug_addrlist
@@ -84,7 +84,7 @@ class unit_tests:
 				('2,4',           '2,4'),
 				('2,4',           '2,4'),
 				('',              ''),
 				('',              ''),
 			):
 			):
-			l = AddrIdxList(i)
+			l = AddrIdxList(fmt_str=i)
 			if cfg.verbose:
 			if cfg.verbose:
 				msg(f'list: {list(l)}\nin:   {i}\nout:  {o}\n')
 				msg(f'list: {list(l)}\nin:   {i}\nout:  {o}\n')
 			assert l.id_str == o, f'{l.id_str} != {o}'
 			assert l.id_str == o, f'{l.id_str} != {o}'

+ 81 - 0
test/modtest_d/ut_amt.py

@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+
+"""
+test.modtest_d.ut_amt: CoinAmt unit tests for the MMGen suite
+"""
+
+from decimal import Decimal
+
+from mmgen.protocol import init_proto
+from mmgen.tx.new import parse_fee_spec
+from mmgen.cfg import Config
+
+from ..include.common import cfg, vmsg
+
+def get_protos(data):
+	return {coin: init_proto(cfg, coin, need_amt=True) for coin in set(d[0] for d in data)}
+
+def test_to_unit(data):
+	protos = get_protos(data)
+	for proto, amt, unit, chk in data:
+		amt = protos[proto].coin_amt(amt)
+		res = amt.to_unit(unit)
+		vmsg(f'  {proto.upper()} {amt.fmt(8)} => {res:<14} {unit}')
+		if '.' in chk:
+			assert res == Decimal(chk), f'{res} != {Decimal(chk)}'
+		else:
+			assert res == int(chk), f'{res} != {int(chk)}'
+	return True
+
+def test_fee_spec(data):
+	protos = get_protos(data)
+	for proto, spec, amt, unit in data:
+		vmsg(f'  {proto.upper():6} {spec:<5} => {amt:<4} {unit}')
+		res = parse_fee_spec(protos[proto], spec)
+		assert res.amt == amt, f'  {res.amt} != {amt}'
+		assert res.unit == unit, f'  {res.unit} != {unit}'
+	return True
+
+class unit_tests:
+
+	altcoin_deps = ('fee_spec_alt', 'to_unit_alt')
+
+	def to_unit(self, name, ut, desc='CoinAmt.to_unit() (BTC)'):
+		return test_to_unit((
+			('btc', '0.00000001', 'satoshi', '1'),
+			('btc', '1.23456789', 'satoshi', '123456789')))
+
+	def to_unit_alt(self, name, ut, desc='CoinAmt.to_unit() (LTC, BCH, ETH, XMR)'):
+		return test_to_unit((
+			('ltc', '0.00000001',           'satoshi', '1'),
+			('ltc', '1.23456789',           'satoshi', '123456789'),
+			('bch', '0.00000001',           'satoshi', '1'),
+			('bch', '1.23456789',           'satoshi', '123456789'),
+			('eth', '1.234567890123456789', 'wei',     '1234567890123456789'),
+			('eth', '1.234567890123456789', 'Kwei',    '1234567890123456.789'),
+			('eth', '1.234567890123456789', 'Mwei',    '1234567890123.456789'),
+			('eth', '1.234567890123456789', 'finney',  '1234.567890123456789'),
+			('eth', '0.000000012345678901', 'Mwei',    '12345.678901'),
+			('eth', '0.000000000000000001', 'Kwei',    '0.001'),
+			('eth', '0.000000000000000001', 'Gwei',    '0.000000001'),
+			('eth', '0.00000001',           'Gwei',    '10'),
+			('eth', '1',                    'Gwei',    '1000000000'),
+			('eth', '1',                    'finney',  '1000'),
+			('xmr', '1',                    'atomic',  '1000000000000'),
+			('xmr', '0.000000000001',       'atomic',  '1'),
+			('xmr', '1.234567890123',       'atomic',  '1234567890123')))
+
+	def fee_spec(self, name, ut, desc='fee spec parsing (BTC)'):
+		return test_fee_spec((
+			('btc', '32s',  '32', 'satoshi'),
+			('btc', '1s',   '1',  'satoshi')))
+
+	def fee_spec_alt(self, name, ut, desc='fee spec parsing (LTC, BCH, ETH, XMR)'):
+		return test_fee_spec((
+			('ltc', '3.07s', '3.07', 'satoshi'),
+			('bch', '3.07s', '3.07', 'satoshi'),
+			('eth', '3.07G', '3.07', 'Gwei'),
+			('eth', '37M',   '37',   'Mwei'),
+			('eth', '3701w', '3701', 'wei'),
+			('eth', '3.07M', '3.07', 'Mwei'),
+			('xmr', '3.07a', '3.07', 'atomic')))

+ 1 - 1
test/modtest_d/ut_bip39.py

@@ -180,7 +180,7 @@ class unit_tests:
 		assert seed_hex == '3c30b98d3d9a713cf5a7a42f5dd27b3bf7f4d792d2b9225f6f519a0da978e13c6f36989ef2123b12a96d6ad5a443a95d61022ffaa9fbce8f946da7b67f75d339'
 		assert seed_hex == '3c30b98d3d9a713cf5a7a42f5dd27b3bf7f4d792d2b9225f6f519a0da978e13c6f36989ef2123b12a96d6ad5a443a95d61022ffaa9fbce8f946da7b67f75d339'
 
 
 		passwd = 'passw0rd'
 		passwd = 'passw0rd'
-		seed_hex = bip39().generate_seed(mnemonic.split(), passwd).hex()
+		seed_hex = bip39().generate_seed(mnemonic.split(), passwd=passwd).hex()
 		vmsg(f'  Password: {orange(passwd)}\n    {seed_hex}')
 		vmsg(f'  Password: {orange(passwd)}\n    {seed_hex}')
 		assert seed_hex == '7eb773bf60f1a5071f96736b6ddbe5c544a7b7740182a80493e29577e58b7cde011d4e38d26f65dab6c9fdebe5594e523447a1427ffd60746e6d04b4daa42eb1'
 		assert seed_hex == '7eb773bf60f1a5071f96736b6ddbe5c544a7b7740182a80493e29577e58b7cde011d4e38d26f65dab6c9fdebe5594e523447a1427ffd60746e6d04b4daa42eb1'
 
 

+ 1 - 1
test/modtest_d/ut_bip_hd.py

@@ -197,7 +197,7 @@ class unit_tests:
 
 
 		coin_type1 = purpose.derive_private()
 		coin_type1 = purpose.derive_private()
 
 
-		coin_type2 = m.to_coin_type('btc', addr_type='bech32')
+		coin_type2 = m.to_coin_type(coin='btc', addr_type='bech32')
 		assert coin_type1.address == coin_type2.address
 		assert coin_type1.address == coin_type2.address
 		vmsg(f'  {coin_type1.address=}')
 		vmsg(f'  {coin_type1.address=}')
 
 

+ 1 - 1
test/modtest_d/ut_gen.py

@@ -62,7 +62,7 @@ def do_test(proto, wif, addr_chk, addr_type, internal_keccak):
 
 
 	for n, backend in enumerate(get_backends(at.pubkey_type)):
 	for n, backend in enumerate(get_backends(at.pubkey_type)):
 
 
-		kg = KeyGenerator( cfg, proto, at.pubkey_type, n+1)
+		kg = KeyGenerator(cfg, proto, at.pubkey_type, silent=n+1)
 		qmsg(blue(f'  Testing backend {backend!r} for addr type {addr_type!r}{add_msg}'))
 		qmsg(blue(f'  Testing backend {backend!r} for addr type {addr_type!r}{add_msg}'))
 
 
 		data = kg.gen_data(privkey)
 		data = kg.gen_data(privkey)

+ 1 - 1
test/modtest_d/ut_misc.py

@@ -50,7 +50,7 @@ class unit_tests:
 		vmsg(brown('  vectors:'))
 		vmsg(brown('  vectors:'))
 		vmsg(fs.format('REL_NOW', 'SHOW_SECS', 'ELAPSED', 'OUTPUT'))
 		vmsg(fs.format('REL_NOW', 'SHOW_SECS', 'ELAPSED', 'OUTPUT'))
 		for (t, now, rel_now, show_secs, out_chk) in vectors:
 		for (t, now, rel_now, show_secs, out_chk) in vectors:
-			out = format_elapsed_hr(t, now, rel_now=rel_now, show_secs=show_secs)
+			out = format_elapsed_hr(t, now=now, rel_now=rel_now, show_secs=show_secs)
 			assert out == out_chk, f'{out} != {out_chk}'
 			assert out == out_chk, f'{out} != {out_chk}'
 			vmsg(fs.format(repr(rel_now), repr(show_secs), now-t, out))
 			vmsg(fs.format(repr(rel_now), repr(show_secs), now-t, out))
 
 

+ 6 - 6
test/modtest_d/ut_seedsplit.py

@@ -66,14 +66,14 @@ class unit_test:
 
 
 				for a, b, c, d, e, f, h, i, p in test_data[id_str if id_str is not None else 'default']:
 				for a, b, c, d, e, f, h, i, p in test_data[id_str if id_str is not None else 'default']:
 					seed_bin = bytes.fromhex('deadbeef' * a)
 					seed_bin = bytes.fromhex('deadbeef' * a)
-					seed = Seed(cfg, seed_bin)
+					seed = Seed(cfg, seed_bin=seed_bin)
 					assert seed.sid == b, seed.sid
 					assert seed.sid == b, seed.sid
 
 
 					for share_count, j, k, l, m in (
 					for share_count, j, k, l, m in (
 							(2, c, c, d, i),
 							(2, c, c, d, i),
 							(5, e, f, h, p)):
 							(5, e, f, h, p)):
 
 
-						shares = seed.split(share_count, id_str, master_idx)
+						shares = seed.split(share_count, id_str=id_str, master_idx=master_idx)
 						A = len(shares)
 						A = len(shares)
 						assert A == share_count, A
 						assert A == share_count, A
 
 
@@ -103,7 +103,7 @@ class unit_test:
 
 
 						if master_idx:
 						if master_idx:
 							slist = [shares.get_share_by_idx(i+1, base_seed=True) for i in range(len(shares))]
 							slist = [shares.get_share_by_idx(i+1, base_seed=True) for i in range(len(shares))]
-							A = Seed.join_shares(cfg, slist, master_idx, id_str).sid
+							A = Seed.join_shares(cfg, slist, master_idx=master_idx, id_str=id_str).sid
 							assert A == b, A
 							assert A == b, A
 
 
 				msg('OK')
 				msg('OK')
@@ -112,7 +112,7 @@ class unit_test:
 			msg_r('Testing defaults and limits...')
 			msg_r('Testing defaults and limits...')
 
 
 			seed_bin = bytes.fromhex('deadbeef' * 8)
 			seed_bin = bytes.fromhex('deadbeef' * 8)
-			seed = Seed(cfg, seed_bin)
+			seed = Seed(cfg, seed_bin=seed_bin)
 
 
 			shares = seed.split(SeedShareIdx.max_val)
 			shares = seed.split(SeedShareIdx.max_val)
 			s = shares.format()
 			s = shares.format()
@@ -136,7 +136,7 @@ class unit_test:
 			vmsg('')
 			vmsg('')
 
 
 			seed_bin = bytes.fromhex(seed_hex)
 			seed_bin = bytes.fromhex(seed_hex)
-			seed = Seed(cfg, seed_bin)
+			seed = Seed(cfg, seed_bin=seed_bin)
 
 
 			SeedShareIdx.max_val = ss_count
 			SeedShareIdx.max_val = ss_count
 			shares = seed.split(ss_count, master_idx=master_idx)
 			shares = seed.split(ss_count, master_idx=master_idx)
@@ -159,7 +159,7 @@ class unit_test:
 			msg_r('Testing last share collisions with shortened Seed IDs')
 			msg_r('Testing last share collisions with shortened Seed IDs')
 			vmsg('')
 			vmsg('')
 			seed_bin = bytes.fromhex('2eadbeef'*8)
 			seed_bin = bytes.fromhex('2eadbeef'*8)
-			seed = Seed(cfg, seed_bin)
+			seed = Seed(cfg, seed_bin=seed_bin)
 			ssm_save = SeedShareIdx.max_val
 			ssm_save = SeedShareIdx.max_val
 			ssm = SeedShareIdx.max_val = 2048
 			ssm = SeedShareIdx.max_val = 2048
 			shares = SeedShareList(seed, count=ssm, id_str='foo', master_idx=1, debug_last_share=True)
 			shares = SeedShareList(seed, count=ssm, id_str='foo', master_idx=1, debug_last_share=True)

+ 8 - 8
test/modtest_d/ut_subseed.py

@@ -25,7 +25,7 @@ class unit_test:
 				):
 				):
 
 
 				seed_bin = bytes.fromhex('deadbeef' * a)
 				seed_bin = bytes.fromhex('deadbeef' * a)
-				seed = Seed(cfg, seed_bin)
+				seed = Seed(cfg, seed_bin=seed_bin)
 				assert seed.sid == b, seed.sid
 				assert seed.sid == b, seed.sid
 
 
 				subseed = seed.subseed('2s')
 				subseed = seed.subseed('2s')
@@ -40,7 +40,7 @@ class unit_test:
 				assert subseed.idx == 10, subseed.idx
 				assert subseed.idx == 10, subseed.idx
 				assert subseed.ss_idx == h, subseed.ss_idx
 				assert subseed.ss_idx == h, subseed.ss_idx
 
 
-				seed2 = Seed(cfg, seed_bin)
+				seed2 = Seed(cfg, seed_bin=seed_bin)
 				ss2_list = seed2.subseeds
 				ss2_list = seed2.subseeds
 
 
 				seed2.subseeds._generate(1)
 				seed2.subseeds._generate(1)
@@ -98,31 +98,31 @@ class unit_test:
 
 
 			seed_bin = bytes.fromhex('deadbeef' * 8)
 			seed_bin = bytes.fromhex('deadbeef' * 8)
 
 
-			seed = Seed(cfg, seed_bin, nSubseeds=11)
+			seed = Seed(cfg, seed_bin=seed_bin, nSubseeds=11)
 			seed.subseeds._generate()
 			seed.subseeds._generate()
 			ss = seed.subseeds
 			ss = seed.subseeds
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert len(ss) == 11, len(ss)
 			assert len(ss) == 11, len(ss)
 
 
-			seed = Seed(cfg, seed_bin)
+			seed = Seed(cfg, seed_bin=seed_bin)
 			seed.subseeds._generate()
 			seed.subseeds._generate()
 			ss = seed.subseeds
 			ss = seed.subseeds
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert len(ss) == nSubseeds, len(ss)
 			assert len(ss) == nSubseeds, len(ss)
 
 
-			seed = Seed(cfg, seed_bin)
+			seed = Seed(cfg, seed_bin=seed_bin)
 			seed.subseed_by_seed_id('EEEEEEEE')
 			seed.subseed_by_seed_id('EEEEEEEE')
 			ss = seed.subseeds
 			ss = seed.subseeds
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert len(ss) == nSubseeds, len(ss)
 			assert len(ss) == nSubseeds, len(ss)
 
 
-			seed = Seed(cfg, seed_bin)
+			seed = Seed(cfg, seed_bin=seed_bin)
 			subseed = seed.subseed_by_seed_id('803B165C')
 			subseed = seed.subseed_by_seed_id('803B165C')
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert subseed.sid == '803B165C', subseed.sid
 			assert subseed.sid == '803B165C', subseed.sid
 			assert subseed.idx == 3, subseed.idx
 			assert subseed.idx == 3, subseed.idx
 
 
-			seed = Seed(cfg, seed_bin)
+			seed = Seed(cfg, seed_bin=seed_bin)
 			subseed = seed.subseed_by_seed_id('803B165C', last_idx=1)
 			subseed = seed.subseed_by_seed_id('803B165C', last_idx=1)
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert len(ss.data['long']) == len(ss.data['short']), len(ss.data['short'])
 			assert subseed is None, subseed
 			assert subseed is None, subseed
@@ -169,7 +169,7 @@ class unit_test:
 			msg_r(f'Testing Seed ID collisions ({ss_count} subseed pairs)...')
 			msg_r(f'Testing Seed ID collisions ({ss_count} subseed pairs)...')
 
 
 			seed_bin = bytes.fromhex('12abcdef' * 8) # 95B3D78D
 			seed_bin = bytes.fromhex('12abcdef' * 8) # 95B3D78D
-			seed = Seed(cfg, seed_bin)
+			seed = Seed(cfg, seed_bin=seed_bin)
 
 
 			seed.subseeds._generate(ss_count)
 			seed.subseeds._generate(ss_count)
 			ss = seed.subseeds
 			ss = seed.subseeds

+ 2 - 2
test/objattrtest_d/oat_btc_mainnet.py

@@ -93,8 +93,8 @@ tests = {
 		'data': (0b00001, bytes),
 		'data': (0b00001, bytes),
 		'sid':  (0b00001, SeedID),
 		'sid':  (0b00001, SeedID),
 		},
 		},
-		[cfg, seed_bin],
-		{},
+		(cfg,),
+		{'seed_bin': seed_bin},
 	),
 	),
 	'SubSeed': atd({
 	'SubSeed': atd({
 		'idx':    (0b00001, int),
 		'idx':    (0b00001, int),

+ 16 - 9
test/objtest_d/ot_btc_mainnet.py

@@ -80,11 +80,17 @@ tests = {
 	},
 	},
 	'AddrIdxList': {
 	'AddrIdxList': {
 		'arg1': 'fmt_str',
 		'arg1': 'fmt_str',
-		'bad': ('x', '5,9,1-2-3', '8,-11', '66,3-2', '0-3'),
+		'bad': (
+			{'fmt_str': 'x'},
+			{'fmt_str': '5,9,1-2-3'},
+			{'fmt_str': '8,-11'},
+			{'fmt_str': '66,3-2'},
+			{'fmt_str': '0-3'},
+		),
 		'good': (
 		'good': (
-			('3,2,2', (2, 3)),
-			('101,1,3,5,2-7,99', (1, 2, 3, 4, 5, 6, 7, 99, 101)),
-			({'idx_list': AddrIdxList('1-5')}, (1, 2, 3, 4, 5))
+			{'fmt_str': '3,2,2', 'ret': (2, 3)},
+			{'fmt_str': '101,1,3,5,2-7,99', 'ret': (1, 2, 3, 4, 5, 6, 7, 99, 101)},
+			{'idx_list': AddrIdxList(fmt_str='1-5'), 'ret': (1, 2, 3, 4, 5)},
 		)
 		)
 	},
 	},
 	'SubSeedIdxRange': {
 	'SubSeedIdxRange': {
@@ -143,13 +149,14 @@ tests = {
 			{'sid': 1},
 			{'sid': 1},
 			{'sid': 'F00BAA123'},
 			{'sid': 'F00BAA123'},
 			{'sid': 'f00baa12'},
 			{'sid': 'f00baa12'},
-			'я', r32, 'abc'
-			),
+			{'seed': r32},
+			{'sid': 'abc'},
+		),
 		'good': (
 		'good': (
 			{'sid': 'F00BAA12'},
 			{'sid': 'F00BAA12'},
-			{'seed': Seed(cfg, r16),     'ret': SeedID(seed=Seed(cfg, r16))},
-			{'sid':  Seed(cfg, r16).sid, 'ret': SeedID(seed=Seed(cfg, r16))}
-			)
+			{'seed': Seed(cfg, seed_bin=r16),     'ret': SeedID(seed=Seed(cfg, seed_bin=r16))},
+			{'sid':  Seed(cfg, seed_bin=r16).sid, 'ret': SeedID(seed=Seed(cfg, seed_bin=r16))}
+		)
 	},
 	},
 	'SubSeedIdx': {
 	'SubSeedIdx': {
 		'arg1': 's',
 		'arg1': 's',

+ 1 - 1
test/overlay/fakemods/mmgen/crypto.py

@@ -12,4 +12,4 @@ if overlay_fake_os.getenv('MMGEN_TEST_SUITE_DETERMINISTIC'):
 		len(overlay_fake_get_random_orig(self, length)))
 		len(overlay_fake_get_random_orig(self, length)))
 
 
 	Crypto.add_user_random = lambda self, rand_bytes, desc: overlay_fake_urandom(
 	Crypto.add_user_random = lambda self, rand_bytes, desc: overlay_fake_urandom(
-		len(overlay_fake_add_user_random_orig(self, rand_bytes, desc)))
+		len(overlay_fake_add_user_random_orig(self, rand_bytes, desc=desc)))

+ 10 - 0
test/overlay/fakemods/mmgen/tx/tx_proxy.py

@@ -0,0 +1,10 @@
+from .tx_proxy_orig import *
+
+class overlay_fake_EtherscanTxProxyClient:
+	proto  = 'http'
+	host   = 'localhost:28800'
+	verify = False
+
+EtherscanTxProxyClient.proto = overlay_fake_EtherscanTxProxyClient.proto
+EtherscanTxProxyClient.host = overlay_fake_EtherscanTxProxyClient.host
+EtherscanTxProxyClient.verify = overlay_fake_EtherscanTxProxyClient.verify

+ 34 - 0
test/ref/ethereum/etherscan-form.html

@@ -0,0 +1,34 @@
+<!doctype html>
+<html id="html" lang="en">
+<head><title>
+	Broadcast Raw Transaction | Etherscan
+</title>
+<meta charset="utf-8" />
+</head>
+<body>
+
+<form action="/search" method="GET">
+	<input type="hidden" value="" id="hdnSearchLabel" />
+</form>
+
+<form method="post" action="./pushTx" id="ctl00" class="js-validate">
+	<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="foo" />
+	<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="bar" />
+	<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="baz" />
+<div>
+	<div>
+		<label for="signedTransactionHex">Enter signed transaction hex</label>
+		<div class="js-form-message">
+		<textarea name="ctl00$ContentPlaceHolder1$txtRawTx" rows="8" cols="20" maxlength="50000" id="ContentPlaceHolder1_txtRawTx" required="" placeholder="e.g. 0x.." data-bg-msg="Please enter signed transaction hex">
+</textarea>
+		</div>
+		<p>Tip: You can also broadcast programatically via our <a href="https://docs.etherscan.io/api-endpoints/geth-parity-proxy" target="_blank">[eth_sendRawTransaction]</a>. Accepts the paramater "hex" for prefilling the input box above (i.e <a href="/pushTx?hex=0x000000">Click here</a>)</p>
+	</div>
+
+	<div class="card-footer bg-light">
+	  <input type="submit" name="ctl00$ContentPlaceHolder1$btnSubmit" value="Send Transaction" id="ContentPlaceHolder1_btnSubmit" class="btn btn-primary" />
+	</div>
+ </div>
+</form>
+</body>
+</html>

+ 33 - 0
test/ref/ethereum/etherscan-result.html

@@ -0,0 +1,33 @@
+<!doctype html>
+<html id="html" lang="en">
+<head><title>
+	Broadcast Raw Transaction | Etherscan
+</title><meta charset="utf-8"/>
+</head>
+<body>
+<form action="/search" method="GET">
+	<input id="hdnIsTestNet" value="False" type="hidden" />
+</form>
+
+<form method="post" action="./pushTx" id="ctl00" class="js-validate">
+<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="foo" />
+<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="bar" />
+<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="baz" />
+	<div>
+	<div>
+		<div role='alert'><button type='button'></button><strong>Success! </strong>{"jsonrpc":"2.0","id":1,"result":"0xbeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafe"}
+<span class='d-block'><i></i> Transaction Hash: <a href='/tx/0xbeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafe'><span class='text-primary'><a href='/tx/0xbeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafe'>0xbeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafe</a></span></b></a></span></div>
+		<label for="signedTransactionHex">Enter signed transaction hex</label>
+		<div>
+		<textarea name="ctl00$ContentPlaceHolder1$txtRawTx" rows="8" cols="20" maxlength="50000" id="ContentPlaceHolder1_txtRawTx" required="" placeholder="e.g. 0x.." data-bg-msg="Please enter signed transaction hex">
+0xdeadbeef</textarea>
+		</div>
+		<p>Tip: You can also broadcast programatically via our <a href="https://docs.etherscan.io/api-endpoints/geth-parity-proxy" target="_blank">[eth_sendRawTransaction]</a>. Accepts the paramater "hex" for prefilling the input box above (i.e <a href="/pushTx?hex=0x000000">Click here</a>)</p>
+	</div>
+	<div>
+	  <input type="submit" name="ctl00$ContentPlaceHolder1$btnSubmit" value="Send Transaction" id="ContentPlaceHolder1_btnSubmit" />
+		</div>
+	 </div>
+  </form>
+</body>
+</html>

+ 16 - 2
test/test-release.d/cfg.sh

@@ -20,7 +20,7 @@ groups_desc="
 
 
 init_groups() {
 init_groups() {
 	dfl_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_tn btc_rt altref altgen bch bch_rt ltc ltc_rt eth etc xmr'
 	dfl_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_tn btc_rt altref altgen bch bch_rt ltc ltc_rt eth etc xmr'
-	extra_tests='dep dev lint autosign_live ltc_tn bch_tn'
+	extra_tests='dep dev lint pylint autosign_live ltc_tn bch_tn'
 	noalt_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_tn btc_rt'
 	noalt_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_tn btc_rt'
 	quick_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_rt altref altgen eth etc xmr'
 	quick_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_rt altref altgen eth etc xmr'
 	qskip_tests='lint btc_tn bch bch_rt ltc ltc_rt'
 	qskip_tests='lint btc_tn bch bch_rt ltc ltc_rt'
@@ -72,7 +72,8 @@ init_tests() {
 	"
 	"
 
 
 	[ "$VERBOSE" ] || STDOUT_DEVNULL='> /dev/null'
 	[ "$VERBOSE" ] || STDOUT_DEVNULL='> /dev/null'
-	d_lint="code errors with static code analyzer"
+	d_lint="code errors with Ruff static code analyzer"
+	e_lint="Error checking failed!"
 	t_lint="
 	t_lint="
 		b ruff check setup.py $STDOUT_DEVNULL
 		b ruff check setup.py $STDOUT_DEVNULL
 		b ruff check mmgen $STDOUT_DEVNULL
 		b ruff check mmgen $STDOUT_DEVNULL
@@ -80,6 +81,19 @@ init_tests() {
 		b ruff check examples $STDOUT_DEVNULL
 		b ruff check examples $STDOUT_DEVNULL
 	"
 	"
 
 
+	PYLINT_OPTS='--errors-only --jobs=0'
+	d_pylint="code errors with Pylint static code analyzer"
+	e_pylint="Error checking failed!"
+	t_pylint="
+		b $pylint $PYLINT_OPTS mmgen
+		b $pylint $PYLINT_OPTS test
+		b $pylint $PYLINT_OPTS --disable=relative-beyond-top-level test/cmdtest_d
+		a $pylint $PYLINT_OPTS --ignore-paths '.*/eth/.*' mmgen
+		a $pylint $PYLINT_OPTS --ignore-paths '.*/ut_dep.py,.*/ut_testdep.py' test
+		a $pylint $PYLINT_OPTS --ignore-paths '.*/ct_ethdev.py' --disable=relative-beyond-top-level test/cmdtest_d
+		- $pylint $PYLINT_OPTS examples
+	"
+
 	d_daemon="low-level subsystems involving coin daemons"
 	d_daemon="low-level subsystems involving coin daemons"
 	t_daemon="- $daemontest_py --exclude exec"
 	t_daemon="- $daemontest_py --exclude exec"
 
 

+ 6 - 3
test/test-release.sh

@@ -18,8 +18,7 @@
 
 
 run_test() {
 run_test() {
 	set +x
 	set +x
-	tests="t_$1"
-	skips="t_$1_skip"
+	local tests="t_$1" skips="t_$1_skip" continue_on_error="e_$1" have_error=
 
 
 	while read skip test; do
 	while read skip test; do
 		[ "$test" ] || continue
 		[ "$test" ] || continue
@@ -35,11 +34,13 @@ run_test() {
 				echo -e "${GREEN}Running:$RESET $test_disp"
 				echo -e "${GREEN}Running:$RESET $test_disp"
 				eval "$test" || {
 				eval "$test" || {
 					echo -e $RED"test-release.sh: test '$CUR_TEST' failed at command '$test'"$RESET
 					echo -e $RED"test-release.sh: test '$CUR_TEST' failed at command '$test'"$RESET
-					exit 1
+					have_error=1
+					[ "${!continue_on_error}" ] || exit 1
 				}
 				}
 			fi
 			fi
 		fi
 		fi
 	done <<<${!tests}
 	done <<<${!tests}
+	if [ "$have_error" ]; then { echo -e "$RED${!continue_on_error}$RESET"; exit 1; }; fi
 }
 }
 
 
 prompt_skip() {
 prompt_skip() {
@@ -276,6 +277,7 @@ gentest_py='test/gentest.py --quiet'
 scrambletest_py='test/scrambletest.py'
 scrambletest_py='test/scrambletest.py'
 altcoin_mod_opts='--quiet'
 altcoin_mod_opts='--quiet'
 mmgen_tool='cmds/mmgen-tool'
 mmgen_tool='cmds/mmgen-tool'
+pylint='PYTHONPATH=. pylint' # PYTHONPATH required by older Pythons (e.g. v3.9)
 python='python3'
 python='python3'
 rounds=10
 rounds=10
 typescript_file='test-release.out'
 typescript_file='test-release.out'
@@ -370,6 +372,7 @@ do
 		tooltest_py+=" --verbose"
 		tooltest_py+=" --verbose"
 		mmgen_tool+=" --verbose"
 		mmgen_tool+=" --verbose"
 		objattrtest_py+=" --verbose"
 		objattrtest_py+=" --verbose"
+		pylint+=" --verbose"
 		scrambletest_py+=" --verbose" ;;
 		scrambletest_py+=" --verbose" ;;
 	X)  IN_REEXEC=1 ;;
 	X)  IN_REEXEC=1 ;;
 	*)  exit ;;
 	*)  exit ;;