From 94bee46cb8de67ff594153c0eb1c9e26de781d8a Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 10 Mar 2025 14:28:55 +0000 Subject: [PATCH] new `mmgen-cli` utility Communicate with all your coin daemons from a single utility! Usage information and examples: $ mmgen-cli --help --- cmds/mmgen-cli | 16 ++++++ doc/wiki/MMGen-Wallet-command-help.md | 1 + doc/wiki/commands/command-help-cli.md | 35 +++++++++++++ mmgen/cfg.py | 1 + mmgen/main_cli.py | 74 +++++++++++++++++++++++++++ mmgen/proto/btc/regtest.py | 11 +--- mmgen/util2.py | 16 ++++++ setup.cfg | 1 + test/cmdtest_d/ct_ethdev.py | 9 ++++ test/cmdtest_d/ct_regtest.py | 23 +++++++-- test/test-release.d/cfg.sh | 6 ++- 11 files changed, 179 insertions(+), 14 deletions(-) create mode 100755 cmds/mmgen-cli create mode 100644 doc/wiki/commands/command-help-cli.md create mode 100755 mmgen/main_cli.py diff --git a/cmds/mmgen-cli b/cmds/mmgen-cli new file mode 100755 index 00000000..f8f8d8b3 --- /dev/null +++ b/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 +# 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') diff --git a/doc/wiki/MMGen-Wallet-command-help.md b/doc/wiki/MMGen-Wallet-command-help.md index 888cdfac..3bdf7f2a 100644 --- a/doc/wiki/MMGen-Wallet-command-help.md +++ b/doc/wiki/MMGen-Wallet-command-help.md @@ -1,6 +1,7 @@ * [mmgen-addrgen](commands/command-help-addrgen.md) * [mmgen-addrimport](commands/command-help-addrimport.md) * [mmgen-autosign](commands/command-help-autosign.md) +* [mmgen-cli](commands/command-help-cli.md) * [mmgen-keygen](commands/command-help-keygen.md) * [mmgen-msg](commands/command-help-msg.md) * [mmgen-passchg](commands/command-help-passchg.md) diff --git a/doc/wiki/commands/command-help-cli.md b/doc/wiki/commands/command-help-cli.md new file mode 100644 index 00000000..d8c953f8 --- /dev/null +++ b/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] + 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) +``` diff --git a/mmgen/cfg.py b/mmgen/cfg.py index bf95d9e4..f9a692bb 100755 --- a/mmgen/cfg.py +++ b/mmgen/cfg.py @@ -62,6 +62,7 @@ class GlobalConstants(Lockable): 'addrgen': _cc(True, False, True, None, [], 'lmw'), 'addrimport': _cc(True, True, True, None, ['tw'], 'lmw'), 'autosign': _cc(True, True, False, '-rRb', ['rpc'], 'lm'), + 'cli': _cc(True, True, True, None, ['tw'], 'lmw'), 'keygen': _cc(True, False, True, None, [], 'lmw'), 'msg': _cc(True, True, True, None, ['msg'], 'lmw'), 'passchg': _cc(False, False, False, None, [], 'lmw'), diff --git a/mmgen/main_cli.py b/mmgen/main_cli.py new file mode 100755 index 00000000..f968c342 --- /dev/null +++ b/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 +# 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] ', + '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()) diff --git a/mmgen/proto/btc/regtest.py b/mmgen/proto/btc/regtest.py index a9d2d672..73b203fa 100755 --- a/mmgen/proto/btc/regtest.py +++ b/mmgen/proto/btc/regtest.py @@ -22,6 +22,7 @@ proto.btc.regtest: Coin daemon regression test mode setup and operations import os, shutil, json from ...util import msg, gmsg, die, capfirst, suf +from ...util2 import cliargs_convert from ...protocol import init_proto from ...rpc import rpc_init, json_encoder from ...objmethods import MMGenObject @@ -46,16 +47,6 @@ def create_data_dir(cfg, data_dir): except: 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): rpc_user = 'bobandalice' diff --git a/mmgen/util2.py b/mmgen/util2.py index cc0700d9..f7e26120 100755 --- a/mmgen/util2.py +++ b/mmgen/util2.py @@ -197,6 +197,22 @@ def decode_pretty_hexdump(data): msg('Data not in hexdump format') 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): 'encode or parse an integer in exponential notation with specified precision' diff --git a/setup.cfg b/setup.cfg index 919441ae..74f66d70 100644 --- a/setup.cfg +++ b/setup.cfg @@ -102,6 +102,7 @@ scripts = cmds/mmgen-addrgen cmds/mmgen-addrimport cmds/mmgen-autosign + cmds/mmgen-cli cmds/mmgen-keygen cmds/mmgen-msg cmds/mmgen-passchg diff --git a/test/cmdtest_d/ct_ethdev.py b/test/cmdtest_d/ct_ethdev.py index 4c814147..7f982064 100755 --- a/test/cmdtest_d/ct_ethdev.py +++ b/test/cmdtest_d/ct_ethdev.py @@ -193,6 +193,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared): ('addrimport', 'importing addresses'), ('addrimport_dev_addr', "importing dev faucet address 'Ox00a329c..'"), ('fund_dev_address', 'funding the default (Parity dev) address'), + ('cli_dev_balance', 'mmgen-cli eth_getBalance'), ), 'msg': ( 'message signing', @@ -568,6 +569,14 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared): t.expect('version') 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): if self.proto.coin == 'ETC': msg(f'skipping test {self.test_name!r} for ETC') diff --git a/test/cmdtest_d/ct_regtest.py b/test/cmdtest_d/ct_regtest.py index 1b157b3a..ec79e65c 100755 --- a/test/cmdtest_d/ct_regtest.py +++ b/test/cmdtest_d/ct_regtest.py @@ -196,6 +196,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared): 'miscellaneous commands', ('daemon_version', 'mmgen-tool daemon_version'), ('halving_calculator_bob', 'halving calculator (Bob)'), + ('cli_txcreate', '‘mmgen-cli createrawtransaction’'), ), 'init_bob': ( 'creating Bob’s MMGen wallet and tracking wallet', @@ -223,7 +224,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared): ('fund_bob', 'funding Bob’s wallet'), ('fund_alice', 'funding Alice’s wallet'), ('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'), ), 'msg': ( @@ -547,6 +548,18 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared): t.expect('time until halving') 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): t = self.spawn('mmgen-walletgen', ['-q', '-r0', '-p1', f'--{user}'], no_passthru_opts=True) t.passphrase_new(f'new {dfl_wcls.desc}', rt_pw) @@ -777,8 +790,12 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared): def alice_bal2(self): 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): return self.user_bal('bob', rtBals[0], self._cashaddr_opt(1)) diff --git a/test/test-release.d/cfg.sh b/test/test-release.d/cfg.sh index 4452d8c0..2dc82202 100755 --- a/test/test-release.d/cfg.sh +++ b/test/test-release.d/cfg.sh @@ -230,7 +230,11 @@ init_tests() { t_ltc_rt="- $cmdtest_py --coin=ltc regtest" 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" t_etc="parity $cmdtest_py --coin=etc ethdev"