Browse Source

new `mmgen-cli` utility

Communicate with all your coin daemons from a single utility!

Usage information and examples:

    $ mmgen-cli --help
The MMGen Project 2 days ago
parent
commit
94bee46cb8

+ 16 - 0
cmds/mmgen-cli

@@ -0,0 +1,16 @@
+#!/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
+
+"""
+mmgen-cli: Communicate with a coin daemon via its JSON-RPC interface
+"""
+
+from mmgen.main import launch
+launch(mod='cli')

+ 1 - 0
doc/wiki/MMGen-Wallet-command-help.md

@@ -1,6 +1,7 @@
 * [mmgen-addrgen](commands/command-help-addrgen.md)
 * [mmgen-addrgen](commands/command-help-addrgen.md)
 * [mmgen-addrimport](commands/command-help-addrimport.md)
 * [mmgen-addrimport](commands/command-help-addrimport.md)
 * [mmgen-autosign](commands/command-help-autosign.md)
 * [mmgen-autosign](commands/command-help-autosign.md)
+* [mmgen-cli](commands/command-help-cli.md)
 * [mmgen-keygen](commands/command-help-keygen.md)
 * [mmgen-keygen](commands/command-help-keygen.md)
 * [mmgen-msg](commands/command-help-msg.md)
 * [mmgen-msg](commands/command-help-msg.md)
 * [mmgen-passchg](commands/command-help-passchg.md)
 * [mmgen-passchg](commands/command-help-passchg.md)

+ 35 - 0
doc/wiki/commands/command-help-cli.md

@@ -0,0 +1,35 @@
+```text
+  MMGEN-CLI: Communicate with a coin daemon via its JSON-RPC interface
+  USAGE:     mmgen-cli [opts] <command> <command args>
+  OPTIONS:
+  -h, --help             Print this help message
+      --longhelp         Print help message for long (global) options
+  -a, --ascii-output     Ensure that output is ASCII encoded
+  -w, --wallet NAME      Use tracking wallet with name NAME
+
+
+  The utility accepts all MMGen global configuration options and sources the user
+  config file, allowing users to preconfigure hosts, ports, passwords, datadirs,
+  tracking wallets and so forth, thus saving a great deal of typing at the
+  command line. This behavior may be overridden with the --skip-cfg-file option.
+
+  Arguments are given in JSON format, with lowercase ‘true’, ‘false’ and ‘null’
+  for booleans and None, and double-quoted strings in dicts and lists.
+
+
+                                     EXAMPLES
+
+    $ mmgen-cli --wallet=wallet2 listreceivedbyaddress 0 true
+
+    $ mmgen-cli --coin=ltc --rpc-host=orion getblockcount
+
+    $ mmgen-cli --regtest=1 --wallet=bob getbalance
+
+    $ mmgen-cli --coin=eth eth_getBalance 0x00000000219ab540356cBB839Cbe05303d7705Fa latest
+
+    $ mmgen-cli createrawtransaction \
+      '[{"txid":"832f5aa9af55dc453314e26869c8f96db1f2a9acac9f23ae18d396903971e0c6","vout":0}]' \
+      '[{"1111111111111111111114oLvT2":0.001}]'
+
+  MMGEN v15.1.dev19              March 2025                       MMGEN-CLI(1)
+```

+ 1 - 0
mmgen/cfg.py

@@ -62,6 +62,7 @@ class GlobalConstants(Lockable):
 		'addrgen':      _cc(True,  False, True,  None,    [],      'lmw'),
 		'addrgen':      _cc(True,  False, True,  None,    [],      'lmw'),
 		'addrimport':   _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
 		'addrimport':   _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
 		'autosign':     _cc(True,  True,  False, '-rRb',  ['rpc'], 'lm'),
 		'autosign':     _cc(True,  True,  False, '-rRb',  ['rpc'], 'lm'),
+		'cli':          _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
 		'keygen':       _cc(True,  False, True,  None,    [],      'lmw'),
 		'keygen':       _cc(True,  False, True,  None,    [],      'lmw'),
 		'msg':          _cc(True,  True,  True,  None,    ['msg'], 'lmw'),
 		'msg':          _cc(True,  True,  True,  None,    ['msg'], 'lmw'),
 		'passchg':      _cc(False, False, False, None,    [],      'lmw'),
 		'passchg':      _cc(False, False, False, None,    [],      'lmw'),

+ 74 - 0
mmgen/main_cli.py

@@ -0,0 +1,74 @@
+#!/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
+
+"""
+mmgen-cli: Communicate with a coin daemon via its JSON-RPC interface
+"""
+
+import asyncio, json
+
+from .util2 import cliargs_convert
+from .cfg import gc, Config
+from .rpc import rpc_init, json_encoder
+
+opts_data = {
+	'text': {
+		'desc':    'Communicate with a coin daemon via its JSON-RPC interface',
+		'usage':   '[opts] <command> <command args>',
+		'options': """
+-h, --help             Print this help message
+--, --longhelp         Print help message for long (global) options
+-a, --ascii-output     Ensure that output is ASCII encoded
+-w, --wallet=NAME      Use tracking wallet with name NAME
+""",
+	'notes': """
+
+The utility accepts all {pn} global configuration options and sources the user
+config file, allowing users to preconfigure hosts, ports, passwords, datadirs,
+tracking wallets and so forth, thus saving a great deal of typing at the
+command line. This behavior may be overridden with the --skip-cfg-file option.
+
+Arguments are given in JSON format, with lowercase ‘true’, ‘false’ and ‘null’
+for booleans and None, and double-quoted strings in dicts and lists.
+
+
+                                   EXAMPLES
+
+  $ mmgen-cli --wallet=wallet2 listreceivedbyaddress 0 true
+
+  $ mmgen-cli --coin=ltc --rpc-host=orion getblockcount
+
+  $ mmgen-cli --regtest=1 --wallet=bob getbalance
+
+  $ mmgen-cli --coin=eth eth_getBalance 0x00000000219ab540356cBB839Cbe05303d7705Fa latest
+
+  $ mmgen-cli createrawtransaction \\
+    '[{{"txid":"832f5aa9af55dc453314e26869c8f96db1f2a9acac9f23ae18d396903971e0c6","vout":0}}]' \\
+    '[{{"1111111111111111111114oLvT2":0.001}}]'
+"""
+	},
+	'code': {
+		'notes': lambda cfg, s, help_notes: s.format(
+			pn = gc.proj_name)
+	}
+}
+
+cfg = Config(opts_data=opts_data)
+cmd, *args = cfg._args
+
+async def main():
+
+	c = await rpc_init(cfg)
+	ret = await c.call(cmd, *cliargs_convert(args), wallet=cfg.wallet)
+	print(
+		(ascii(ret) if cfg.ascii_output else ret) if isinstance(ret, str) else
+		json.dumps(ret, cls=json_encoder, indent=4, ensure_ascii=cfg.ascii_output))
+
+asyncio.run(main())

+ 1 - 10
mmgen/proto/btc/regtest.py

@@ -22,6 +22,7 @@ proto.btc.regtest: Coin daemon regression test mode setup and operations
 
 
 import os, shutil, json
 import os, shutil, json
 from ...util import msg, gmsg, die, capfirst, suf
 from ...util import msg, gmsg, die, capfirst, suf
+from ...util2 import cliargs_convert
 from ...protocol import init_proto
 from ...protocol import init_proto
 from ...rpc import rpc_init, json_encoder
 from ...rpc import rpc_init, json_encoder
 from ...objmethods import MMGenObject
 from ...objmethods import MMGenObject
@@ -46,16 +47,6 @@ def create_data_dir(cfg, data_dir):
 	except:
 	except:
 		pass
 		pass
 
 
-def cliargs_convert(args):
-	def gen():
-		for arg in args:
-			try:
-				yield json.loads(arg) # list, dict, bool, int, null
-			except:
-				yield arg # arbitrary string
-
-	return tuple(gen())
-
 class MMGenRegtest(MMGenObject):
 class MMGenRegtest(MMGenObject):
 
 
 	rpc_user     = 'bobandalice'
 	rpc_user     = 'bobandalice'

+ 16 - 0
mmgen/util2.py

@@ -197,6 +197,22 @@ def decode_pretty_hexdump(data):
 		msg('Data not in hexdump format')
 		msg('Data not in hexdump format')
 		return False
 		return False
 
 
+def cliargs_convert(args):
+
+	# return str instead of float for input into JSON-RPC
+	def float_parser(n):
+		return n
+
+	import json
+	def gen():
+		for arg in args:
+			try:
+				yield json.loads(arg, parse_float=float_parser) # list, dict, bool, int, null, float
+			except json.decoder.JSONDecodeError:
+				yield arg # arbitrary string
+
+	return tuple(gen())
+
 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'
 
 

+ 1 - 0
setup.cfg

@@ -102,6 +102,7 @@ scripts =
 	cmds/mmgen-addrgen
 	cmds/mmgen-addrgen
 	cmds/mmgen-addrimport
 	cmds/mmgen-addrimport
 	cmds/mmgen-autosign
 	cmds/mmgen-autosign
+	cmds/mmgen-cli
 	cmds/mmgen-keygen
 	cmds/mmgen-keygen
 	cmds/mmgen-msg
 	cmds/mmgen-msg
 	cmds/mmgen-passchg
 	cmds/mmgen-passchg

+ 9 - 0
test/cmdtest_d/ct_ethdev.py

@@ -193,6 +193,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		('addrimport',          'importing addresses'),
 		('addrimport',          'importing addresses'),
 		('addrimport_dev_addr', "importing dev faucet address 'Ox00a329c..'"),
 		('addrimport_dev_addr', "importing dev faucet address 'Ox00a329c..'"),
 		('fund_dev_address',    'funding the default (Parity dev) address'),
 		('fund_dev_address',    'funding the default (Parity dev) address'),
+		('cli_dev_balance',      'mmgen-cli eth_getBalance'),
 	),
 	),
 	'msg': (
 	'msg': (
 		'message signing',
 		'message signing',
@@ -568,6 +569,14 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		t.expect('version')
 		t.expect('version')
 		return t
 		return t
 
 
+	def cli_dev_balance(self):
+		t = self.spawn(
+			'mmgen-cli',
+			[f'--coin={self.proto.coin}', '--regtest=1', 'eth_getBalance', '0x'+dfl_devaddr, 'latest'])
+		if self.daemon.id == 'geth':
+			t.expect('0x33b2e3c91ec0e9113986000')
+		return t
+
 	async def _wallet_upgrade(self, src_fn, expect1, expect2=None):
 	async def _wallet_upgrade(self, src_fn, expect1, expect2=None):
 		if self.proto.coin == 'ETC':
 		if self.proto.coin == 'ETC':
 			msg(f'skipping test {self.test_name!r} for ETC')
 			msg(f'skipping test {self.test_name!r} for ETC')

+ 20 - 3
test/cmdtest_d/ct_regtest.py

@@ -196,6 +196,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		'miscellaneous commands',
 		'miscellaneous commands',
 		('daemon_version',         'mmgen-tool daemon_version'),
 		('daemon_version',         'mmgen-tool daemon_version'),
 		('halving_calculator_bob', 'halving calculator (Bob)'),
 		('halving_calculator_bob', 'halving calculator (Bob)'),
+		('cli_txcreate',           '‘mmgen-cli createrawtransaction’'),
 	),
 	),
 	'init_bob': (
 	'init_bob': (
 		'creating Bob’s MMGen wallet and tracking wallet',
 		'creating Bob’s MMGen wallet and tracking wallet',
@@ -223,7 +224,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',                     'Bob’s balance'),
+		('bob_bal1_cli',                 'Bob’s balance (via ‘mmgen-cli’)'),
 		('generate_extra_deterministic', 'generate extra blocks for deterministic run'),
 		('generate_extra_deterministic', 'generate extra blocks for deterministic run'),
 	),
 	),
 	'msg': (
 	'msg': (
@@ -547,6 +548,18 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		t.expect('time until halving')
 		t.expect('time until halving')
 		return t
 		return t
 
 
+	def cli_txcreate(self):
+		txid = 'beadcafe' * 8
+		return self.spawn(
+			'mmgen-cli',
+			[
+				'--regtest=1',
+				f'--coin={self.proto.coin}',
+				'createrawtransaction',
+				f'[{{"txid":"{txid}","vout":7}}]',
+				f'[{{"{self.burn_addr}":0.001}}]'
+			])
+
 	def walletgen(self, user):
 	def walletgen(self, user):
 		t = self.spawn('mmgen-walletgen', ['-q', '-r0', '-p1', f'--{user}'], no_passthru_opts=True)
 		t = self.spawn('mmgen-walletgen', ['-q', '-r0', '-p1', f'--{user}'], no_passthru_opts=True)
 		t.passphrase_new(f'new {dfl_wcls.desc}', rt_pw)
 		t.passphrase_new(f'new {dfl_wcls.desc}', rt_pw)
@@ -777,8 +790,12 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 	def alice_bal2(self):
 	def alice_bal2(self):
 		return self.user_bal('alice', rtBals[8])
 		return self.user_bal('alice', rtBals[8])
 
 
-	def bob_bal1(self):
-		return self.user_bal('bob', rtFundAmt, self._cashaddr_opt(0))
+	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_bal2(self):
 	def bob_bal2(self):
 		return self.user_bal('bob', rtBals[0], self._cashaddr_opt(1))
 		return self.user_bal('bob', rtBals[0], self._cashaddr_opt(1))

+ 5 - 1
test/test-release.d/cfg.sh

@@ -230,7 +230,11 @@ init_tests() {
 	t_ltc_rt="- $cmdtest_py --coin=ltc regtest"
 	t_ltc_rt="- $cmdtest_py --coin=ltc regtest"
 
 
 	d_eth="operations for Ethereum using devnet"
 	d_eth="operations for Ethereum using devnet"
-	t_eth="geth $cmdtest_py --coin=eth ethdev"
+	t_eth="
+		geth $cmdtest_py --coin=eth ethdev
+		reth $cmdtest_py --coin=eth --daemon-id=reth ethdev
+	"
+	[ "$FAST" ]  && t_eth_skip='reth'
 
 
 	d_etc="operations for Ethereum Classic using devnet"
 	d_etc="operations for Ethereum Classic using devnet"
 	t_etc="parity $cmdtest_py --coin=etc ethdev"
 	t_etc="parity $cmdtest_py --coin=etc ethdev"