opts, help: refactor, parse cmdline opts natively, filter global opts
- command-line options are now parsed natively, without use of the getopt module
- global options and --longhelp helpscreen are now contextual, depending on coin
and executed command
- commands invoked with out-of-context global options (e.g. `mmgen-walletgen
--coin=btc`) now fail with an ‘unrecognized option’ error
Testing:
$ test/test-release.sh help
$ test/cmdtest.py opts
This commit is contained in:
parent
307e6fb541
commit
d7e3b55e3b
14 changed files with 785 additions and 565 deletions
12
mmgen/cfg.py
12
mmgen/cfg.py
|
|
@ -51,7 +51,10 @@ class GlobalConstants(Lockable):
|
|||
min_time_precision = 18
|
||||
|
||||
# must match CoinProtocol.coins
|
||||
core_coins = ('btc','bch','ltc','eth','etc','zec','xmr')
|
||||
core_coins = ('btc', 'bch', 'ltc', 'eth', 'etc', 'zec', 'xmr')
|
||||
rpc_coins = ('btc', 'bch', 'ltc', 'eth', 'etc', 'xmr')
|
||||
btc_fork_rpc_coins = ('btc', 'bch', 'ltc')
|
||||
eth_fork_coins = ('eth', 'etc')
|
||||
|
||||
_cc = namedtuple('cmd_cap', ['proto', 'rpc', 'coin', 'caps', 'platforms'])
|
||||
cmd_caps_data = {
|
||||
|
|
@ -461,8 +464,8 @@ class Config(Lockable):
|
|||
opts_data = opts_data,
|
||||
init_opts = init_opts,
|
||||
opt_filter = opt_filter,
|
||||
parse_only = parse_only,
|
||||
parsed_opts = parsed_opts )
|
||||
parsed_opts = parsed_opts,
|
||||
need_proto = need_proto)
|
||||
self._uopt_desc = 'command-line option'
|
||||
else:
|
||||
if cfg is None:
|
||||
|
|
@ -575,7 +578,8 @@ class Config(Lockable):
|
|||
|
||||
def _post_init(self):
|
||||
if self.help or self.longhelp:
|
||||
self._opts.init_bottom(self) # exits
|
||||
from .help import print_help
|
||||
print_help(self, self._opts) # exits
|
||||
del self._opts
|
||||
|
||||
def _usage(self):
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
September 2024
|
||||
October 2024
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
15.1.dev1
|
||||
15.1.dev2
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class FileNotFound(Exception): mmcode = 1
|
|||
class InvalidPasswdFormat(Exception): mmcode = 1
|
||||
class CfgFileParseError(Exception): mmcode = 1
|
||||
class UserOptError(Exception): mmcode = 1
|
||||
class CmdlineOptError(Exception): mmcode = 1
|
||||
class NoLEDSupport(Exception): mmcode = 1
|
||||
class MsgFileFailedSID(Exception): mmcode = 1
|
||||
class TestSuiteException(Exception): mmcode = 1
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
help: help notes for MMGen suite commands
|
||||
"""
|
||||
|
||||
import sys
|
||||
import sys, re
|
||||
|
||||
from ..cfg import gc
|
||||
|
||||
|
|
@ -60,261 +60,110 @@ def usage(cfg):
|
|||
print(make_usage_str(cfg, caller='user'))
|
||||
sys.exit(0)
|
||||
|
||||
def help_notes_func(proto,cfg,k):
|
||||
class Help:
|
||||
|
||||
def fee_spec_letters(use_quotes=False):
|
||||
cu = proto.coin_amt.units
|
||||
sep,conj = ((',',' or '),("','","' or '"))[use_quotes]
|
||||
return sep.join(u[0] for u in cu[:-1]) + ('',conj)[len(cu)>1] + cu[-1][0]
|
||||
def make(self, cfg, opts, proto):
|
||||
|
||||
def fee_spec_names():
|
||||
cu = proto.coin_amt.units
|
||||
return ', '.join(cu[:-1]) + ('',' and ')[len(cu)>1] + cu[-1] + ('',',\nrespectively')[len(cu)>1]
|
||||
def gen_arg_tuple(func, text):
|
||||
|
||||
def coind_exec():
|
||||
from ..daemon import CoinDaemon
|
||||
return (
|
||||
CoinDaemon(cfg,proto.coin).exec_fn if proto.coin in CoinDaemon.coins else 'bitcoind' )
|
||||
def help_notes(k):
|
||||
import importlib
|
||||
return getattr(importlib.import_module(
|
||||
f'{opts.help_pkg}.help_notes').help_notes(proto, cfg), k)()
|
||||
|
||||
class help_notes:
|
||||
def help_mod(modname):
|
||||
import importlib
|
||||
return importlib.import_module(
|
||||
f'{opts.help_pkg}.{modname}').help(proto, cfg)
|
||||
|
||||
def dfl_twname():
|
||||
from ..proto.btc.rpc import BitcoinRPCClient
|
||||
return BitcoinRPCClient.dfl_twname
|
||||
d = {
|
||||
'proto': proto,
|
||||
'help_notes': help_notes,
|
||||
'help_mod': help_mod,
|
||||
'cfg': cfg,
|
||||
}
|
||||
for arg in func.__code__.co_varnames:
|
||||
yield d[arg] if arg in d else text
|
||||
|
||||
def MasterShareIdx():
|
||||
from ..seedsplit import MasterShareIdx
|
||||
return MasterShareIdx
|
||||
def gen_output():
|
||||
yield ' {} {}'.format(gc.prog_name.upper() + ':', text['desc'].strip())
|
||||
yield make_usage_str(cfg, caller='help')
|
||||
yield help_type.upper().replace('_', ' ') + ':'
|
||||
|
||||
def tool_help():
|
||||
from ..tool.help import main_help
|
||||
return main_help()
|
||||
# process code for options
|
||||
opts_text = nl.join(self.gen_text(opts))
|
||||
if help_type in code:
|
||||
yield code[help_type](*tuple(gen_arg_tuple(code[help_type], opts_text)))
|
||||
else:
|
||||
yield opts_text
|
||||
|
||||
def dfl_subseeds():
|
||||
from ..subseed import SubSeedList
|
||||
return str(SubSeedList.dfl_len)
|
||||
# process code for notes
|
||||
if help_type == 'options' and 'notes' in text:
|
||||
if 'notes' in code:
|
||||
yield from code['notes'](*tuple(gen_arg_tuple(code['notes'], text['notes']))).splitlines()
|
||||
else:
|
||||
yield from text['notes'].splitlines()
|
||||
|
||||
def dfl_seed_len():
|
||||
from ..seed import Seed
|
||||
return str(Seed.dfl_len)
|
||||
text = opts.opts_data['text']
|
||||
code = opts.opts_data['code']
|
||||
help_type = self.help_type
|
||||
nl = '\n '
|
||||
|
||||
def password_formats():
|
||||
from ..passwdlist import PasswordList
|
||||
pwi_fs = '{:8} {:1} {:26} {:<7} {:<7} {}'
|
||||
return '\n '.join(
|
||||
[pwi_fs.format('Code','','Description','Min Len','Max Len','Default Len')] +
|
||||
[pwi_fs.format(k,'-',v.desc,v.min_len,v.max_len,v.dfl_len) for k,v in PasswordList.pw_info.items()]
|
||||
)
|
||||
return nl.join(gen_output()) + '\n'
|
||||
|
||||
def dfl_mmtype():
|
||||
from ..addr import MMGenAddrType
|
||||
return "'{}' or '{}'".format(
|
||||
proto.dfl_mmtype,
|
||||
MMGenAddrType.mmtypes[proto.dfl_mmtype].name )
|
||||
class CmdHelp(Help):
|
||||
|
||||
def address_types():
|
||||
from ..addr import MMGenAddrType
|
||||
return '\n '.join([
|
||||
"'{}','{:<12} - {}".format( k, v.name+"'", v.desc )
|
||||
for k,v in MMGenAddrType.mmtypes.items()
|
||||
])
|
||||
help_type = 'options'
|
||||
|
||||
def fmt_codes():
|
||||
from ..wallet import format_fmt_codes
|
||||
return '\n '.join( format_fmt_codes().splitlines() )
|
||||
def gen_text(self, opts):
|
||||
opt_filter = opts.opt_filter
|
||||
from ..opts import cmd_opts_pat
|
||||
skipping = False
|
||||
for line in opts.opts_data['text']['options'].strip().splitlines():
|
||||
if m := cmd_opts_pat.match(line):
|
||||
if opt_filter:
|
||||
if m[1] in opt_filter:
|
||||
skipping = False
|
||||
else:
|
||||
skipping = True
|
||||
continue
|
||||
yield '{} --{} {}'.format(
|
||||
(f'-{m[1]},', ' ')[m[1] == '-'],
|
||||
m[2],
|
||||
m[4])
|
||||
elif not skipping:
|
||||
yield line
|
||||
|
||||
def coin_id():
|
||||
return proto.coin_id
|
||||
class GlobalHelp(Help):
|
||||
|
||||
def keygen_backends():
|
||||
from ..keygen import get_backends
|
||||
from ..addr import MMGenAddrType
|
||||
backends = get_backends(
|
||||
MMGenAddrType(proto,cfg.type or proto.dfl_mmtype).pubkey_type
|
||||
)
|
||||
return ' '.join( f'{n}:{k}{" [default]" if n==1 else ""}' for n,k in enumerate(backends,1) )
|
||||
help_type = 'global_options'
|
||||
|
||||
def coind_exec():
|
||||
return coind_exec()
|
||||
def gen_text(self, opts):
|
||||
from ..opts import global_opts_pat
|
||||
for line in opts.global_opts_data['text'][1:-2].splitlines():
|
||||
if m := global_opts_pat.match(line):
|
||||
if m[1] in opts.global_opts_filter.coin and m[2] in opts.global_opts_filter.cmd:
|
||||
yield ' --{} {}'.format(m[3], m[5])
|
||||
skipping = False
|
||||
else:
|
||||
skipping = True
|
||||
elif not skipping:
|
||||
yield line[4:]
|
||||
|
||||
def coin_daemon_network_ids():
|
||||
from ..daemon import CoinDaemon
|
||||
from ..util import fmt_list
|
||||
return fmt_list(CoinDaemon.get_network_ids(cfg),fmt='bare')
|
||||
def print_help(cfg, opts):
|
||||
|
||||
def rel_fee_desc():
|
||||
from ..tx import BaseTX
|
||||
return BaseTX(cfg=cfg,proto=proto).rel_fee_desc
|
||||
from ..protocol import init_proto_from_cfg
|
||||
proto = init_proto_from_cfg(cfg, need_amt=True)
|
||||
|
||||
def fee_spec_letters():
|
||||
return fee_spec_letters()
|
||||
if not 'code' in opts.opts_data:
|
||||
opts.opts_data['code'] = {}
|
||||
|
||||
def fee():
|
||||
from ..tx import BaseTX
|
||||
return """
|
||||
FEE SPECIFICATION
|
||||
if cfg.help:
|
||||
cls = CmdHelp
|
||||
else:
|
||||
opts.opts_data['code']['global_options'] = opts.global_opts_data['code']
|
||||
cls = GlobalHelp
|
||||
|
||||
Transaction fees, both on the command line and at the interactive prompt, may
|
||||
be specified as either absolute {c} amounts, using a plain decimal number, or
|
||||
as {r}, using an integer followed by '{l}', for {u}.
|
||||
""".format(
|
||||
c = proto.coin,
|
||||
r = BaseTX(cfg=cfg,proto=proto).rel_fee_desc,
|
||||
l = fee_spec_letters(use_quotes=True),
|
||||
u = fee_spec_names() )
|
||||
|
||||
def passwd():
|
||||
return """
|
||||
PASSPHRASE NOTE:
|
||||
|
||||
For passphrases all combinations of whitespace are equal, and leading and
|
||||
trailing space are ignored. This permits reading passphrase or brainwallet
|
||||
data from a multi-line file with free spacing and indentation.
|
||||
""".strip()
|
||||
|
||||
def brainwallet():
|
||||
return """
|
||||
BRAINWALLET NOTE:
|
||||
|
||||
To thwart dictionary attacks, it’s recommended to use a strong hash preset
|
||||
with brainwallets. For a brainwallet passphrase to generate the correct
|
||||
seed, the same seed length and hash preset parameters must always be used.
|
||||
""".strip()
|
||||
|
||||
def txcreate_examples():
|
||||
|
||||
mmtype = 'B' if 'B' in proto.mmtypes else proto.mmtypes[0]
|
||||
from ..tool.coin import tool_cmd
|
||||
t = tool_cmd(cfg, mmtype=mmtype)
|
||||
addr = t.privhex2addr('bead' * 16)
|
||||
sample_addr = addr.views[addr.view_pref]
|
||||
|
||||
return f"""
|
||||
EXAMPLES:
|
||||
|
||||
Send 0.123 {proto.coin} to an external {proto.name} address, returning the change to a
|
||||
specific MMGen address in the tracking wallet:
|
||||
|
||||
$ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}:7
|
||||
|
||||
Same as above, but select the change address automatically:
|
||||
|
||||
$ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}
|
||||
|
||||
Same as above, but select the change address automatically by address type:
|
||||
|
||||
$ {gc.prog_name} {sample_addr},0.123 {mmtype}
|
||||
|
||||
Same as above, but reduce verbosity and specify fee of 20 satoshis
|
||||
per byte:
|
||||
|
||||
$ {gc.prog_name} -q -f 20s {sample_addr},0.123 {mmtype}
|
||||
|
||||
Send entire balance of selected inputs minus fee to an external {proto.name}
|
||||
address:
|
||||
|
||||
$ {gc.prog_name} {sample_addr}
|
||||
|
||||
Send entire balance of selected inputs minus fee to first unused wallet
|
||||
address of specified type:
|
||||
|
||||
$ {gc.prog_name} {mmtype}
|
||||
"""
|
||||
|
||||
def txcreate():
|
||||
return f"""
|
||||
The transaction’s outputs are listed on the command line, while its inputs
|
||||
are chosen from a list of the wallet’s unspent outputs via an interactive
|
||||
menu. Alternatively, inputs may be specified using the --inputs option.
|
||||
|
||||
All addresses on the command line can be either {proto.name} addresses or MMGen
|
||||
IDs in the form <seed ID>:<address type letter>:<index>.
|
||||
|
||||
Outputs are specified in the form <address>,<amount>, with the change output
|
||||
specified by address only. Alternatively, the change output may be an
|
||||
addrlist ID in the form <seed ID>:<address type letter>, in which case the
|
||||
first unused address in the tracking wallet matching the requested ID will
|
||||
be automatically selected as the change output.
|
||||
|
||||
If the transaction fee is not specified on the command line (see FEE
|
||||
SPECIFICATION below), it will be calculated dynamically using network fee
|
||||
estimation for the default (or user-specified) number of confirmations.
|
||||
If network fee estimation fails, the user will be prompted for a fee.
|
||||
|
||||
Network-estimated fees will be multiplied by the value of --fee-adjust, if
|
||||
specified.
|
||||
|
||||
To send the value of all inputs (minus TX fee) to a single output, specify
|
||||
a single address with no amount on the command line. Alternatively, an
|
||||
addrlist ID may be specified, and the address will be chosen automatically
|
||||
as described above for the change output.
|
||||
"""
|
||||
|
||||
def txsign():
|
||||
from ..proto.btc.params import mainnet
|
||||
return """
|
||||
Transactions may contain both {pnm} or non-{pnm} input addresses.
|
||||
|
||||
To sign non-{pnm} inputs, a {wd}flat key list is used
|
||||
as the key source (--keys-from-file option).
|
||||
|
||||
To sign {pnm} inputs, key data is generated from a seed as with the
|
||||
{pnl}-addrgen and {pnl}-keygen commands. Alternatively, a key-address file
|
||||
may be used (--mmgen-keys-from-file option).
|
||||
|
||||
Multiple wallets or other seed files can be listed on the command line in
|
||||
any order. If the seeds required to sign the transaction’s inputs are not
|
||||
found in these files (or in the default wallet), the user will be prompted
|
||||
for seed data interactively.
|
||||
|
||||
To prevent an attacker from crafting transactions with bogus {pnm}-to-{pnu}
|
||||
address mappings, all outputs to {pnm} addresses are verified with a seed
|
||||
source. Therefore, seed files or a key-address file for all {pnm} outputs
|
||||
must also be supplied on the command line if the data can’t be found in the
|
||||
default wallet.
|
||||
""".format(
|
||||
wd = (f'{coind_exec()} wallet dump or ' if isinstance(proto,mainnet) else ''),
|
||||
pnm = gc.proj_name,
|
||||
pnu = proto.name,
|
||||
pnl = gc.proj_name.lower() )
|
||||
|
||||
def subwallet():
|
||||
from ..subseed import SubSeedIdxRange
|
||||
return f"""
|
||||
SUBWALLETS:
|
||||
|
||||
Subwallets (subseeds) are specified by a ‘Subseed Index’ consisting of:
|
||||
|
||||
a) an integer in the range 1-{SubSeedIdxRange.max_idx}, plus
|
||||
b) an optional single letter, ‘L’ or ‘S’
|
||||
|
||||
The letter designates the length of the subseed. If omitted, ‘L’ is assumed.
|
||||
|
||||
Long (‘L’) subseeds are the same length as their parent wallet’s seed
|
||||
(typically 256 bits), while short (‘S’) subseeds are always 128-bit.
|
||||
The long and short subseeds for a given index are derived independently,
|
||||
so both may be used.
|
||||
|
||||
MMGen Wallet has no notion of ‘depth’, and to an outside observer subwallets
|
||||
are identical to ordinary wallets. This is a feature rather than a bug, as
|
||||
it denies an attacker any way of knowing whether a given wallet has a parent.
|
||||
|
||||
Since subwallets are just wallets, they may be used to generate other
|
||||
subwallets, leading to hierarchies of arbitrary depth. However, this is
|
||||
inadvisable in practice for two reasons: Firstly, it creates accounting
|
||||
complexity, requiring the user to independently keep track of a derivation
|
||||
tree. More importantly, however, it leads to the danger of Seed ID
|
||||
collisions between subseeds at different levels of the hierarchy, as
|
||||
MMGen checks and avoids ID collisions only among sibling subseeds.
|
||||
|
||||
An exception to this caveat would be a multi-user setup where sibling
|
||||
subwallets are distributed to different users as their default wallets.
|
||||
Since the subseeds derived from these subwallets are private to each user,
|
||||
Seed ID collisions among them doesn’t present a problem.
|
||||
|
||||
A safe rule of thumb, therefore, is for *each user* to derive all of his/her
|
||||
subwallets from a single parent. This leaves each user with a total of two
|
||||
million subwallets, which should be enough for most practical purposes.
|
||||
""".strip()
|
||||
|
||||
return getattr(help_notes,k)()
|
||||
from ..ui import do_pager
|
||||
do_pager(cls().make(cfg, opts, proto))
|
||||
sys.exit(0)
|
||||
|
|
|
|||
268
mmgen/help/help_notes.py
Executable file
268
mmgen/help/help_notes.py
Executable file
|
|
@ -0,0 +1,268 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet
|
||||
# https://gitlab.com/mmgen/mmgen-wallet
|
||||
|
||||
"""
|
||||
help: help notes functions for MMGen suite commands
|
||||
"""
|
||||
|
||||
from ..cfg import gc
|
||||
|
||||
class help_notes:
|
||||
|
||||
def __init__(self, proto, cfg):
|
||||
self.proto = proto
|
||||
self.cfg = cfg
|
||||
|
||||
def fee_spec_letters(self, use_quotes=False):
|
||||
cu = self.proto.coin_amt.units
|
||||
sep, conj = ((',', ' or '), ("','", "' or '"))[use_quotes]
|
||||
return sep.join(u[0] for u in cu[:-1]) + ('', conj)[len(cu)>1] + cu[-1][0]
|
||||
|
||||
def fee_spec_names(self):
|
||||
cu = self.proto.coin_amt.units
|
||||
return ', '.join(cu[:-1]) + ('', ' and ')[len(cu)>1] + cu[-1] + ('', ',\nrespectively')[len(cu)>1]
|
||||
|
||||
def coind_exec(self):
|
||||
from ..daemon import CoinDaemon
|
||||
return (
|
||||
CoinDaemon(self.cfg, self.proto.coin).exec_fn if self.proto.coin in CoinDaemon.coins else 'bitcoind')
|
||||
|
||||
def dfl_twname(self):
|
||||
from ..proto.btc.rpc import BitcoinRPCClient
|
||||
return BitcoinRPCClient.dfl_twname
|
||||
|
||||
def MasterShareIdx(self):
|
||||
from ..seedsplit import MasterShareIdx
|
||||
return MasterShareIdx
|
||||
|
||||
def tool_help(self):
|
||||
from ..tool.help import main_help
|
||||
return main_help()
|
||||
|
||||
def dfl_subseeds(self):
|
||||
from ..subseed import SubSeedList
|
||||
return str(SubSeedList.dfl_len)
|
||||
|
||||
def dfl_seed_len(self):
|
||||
from ..seed import Seed
|
||||
return str(Seed.dfl_len)
|
||||
|
||||
def password_formats(self):
|
||||
from ..passwdlist import PasswordList
|
||||
pwi_fs = '{:8} {:1} {:26} {:<7} {:<7} {}'
|
||||
return '\n '.join(
|
||||
[pwi_fs.format('Code', '','Description', 'Min Len', 'Max Len', 'Default Len')] +
|
||||
[pwi_fs.format(k, '-', v.desc, v.min_len, v.max_len, v.dfl_len)
|
||||
for k, v in PasswordList.pw_info.items()]
|
||||
)
|
||||
|
||||
def dfl_mmtype(self):
|
||||
from ..addr import MMGenAddrType
|
||||
return "'{}' or '{}'".format(self.proto.dfl_mmtype, MMGenAddrType.mmtypes[self.proto.dfl_mmtype].name)
|
||||
|
||||
def address_types(self):
|
||||
from ..addr import MMGenAddrType
|
||||
return '\n '.join([
|
||||
"'{}','{:<12} - {}".format(k, v.name + "'", v.desc)
|
||||
for k, v in MMGenAddrType.mmtypes.items()
|
||||
])
|
||||
|
||||
def fmt_codes(self):
|
||||
from ..wallet import format_fmt_codes
|
||||
return '\n '.join(format_fmt_codes().splitlines())
|
||||
|
||||
def coin_id(self):
|
||||
return self.proto.coin_id
|
||||
|
||||
def keygen_backends(self):
|
||||
from ..keygen import get_backends
|
||||
from ..addr import MMGenAddrType
|
||||
backends = get_backends(
|
||||
MMGenAddrType(self.proto, self.cfg.type or self.proto.dfl_mmtype).pubkey_type
|
||||
)
|
||||
return ' '.join('{n}:{k}{t}'.format(n=n, k=k, t=('', ' [default]')[n == 1])
|
||||
for n, k in enumerate(backends, 1))
|
||||
|
||||
def coin_daemon_network_ids(self):
|
||||
from ..daemon import CoinDaemon
|
||||
from ..util import fmt_list
|
||||
return fmt_list(CoinDaemon.get_network_ids(self.cfg), fmt='bare')
|
||||
|
||||
def rel_fee_desc(self):
|
||||
from ..tx import BaseTX
|
||||
return BaseTX(cfg=self.cfg, proto=self.proto).rel_fee_desc
|
||||
|
||||
def fee(self):
|
||||
from ..tx import BaseTX
|
||||
return """
|
||||
FEE SPECIFICATION
|
||||
|
||||
Transaction fees, both on the command line and at the interactive prompt, may
|
||||
be specified as either absolute {c} amounts, using a plain decimal number, or
|
||||
as {r}, using an integer followed by '{l}', for {u}.
|
||||
""".format(
|
||||
c = self.proto.coin,
|
||||
r = BaseTX(cfg=self.cfg, proto=self.proto).rel_fee_desc,
|
||||
l = self.fee_spec_letters(use_quotes=True),
|
||||
u = self.fee_spec_names() )
|
||||
|
||||
def passwd(self):
|
||||
return """
|
||||
PASSPHRASE NOTE:
|
||||
|
||||
For passphrases all combinations of whitespace are equal, and leading and
|
||||
trailing space are ignored. This permits reading passphrase or brainwallet
|
||||
data from a multi-line file with free spacing and indentation.
|
||||
""".strip()
|
||||
|
||||
def brainwallet(self):
|
||||
return """
|
||||
BRAINWALLET NOTE:
|
||||
|
||||
To thwart dictionary attacks, it’s recommended to use a strong hash preset
|
||||
with brainwallets. For a brainwallet passphrase to generate the correct
|
||||
seed, the same seed length and hash preset parameters must always be used.
|
||||
""".strip()
|
||||
|
||||
def txcreate_examples(self):
|
||||
|
||||
mmtype = 'B' if 'B' in self.proto.mmtypes else self.proto.mmtypes[0]
|
||||
from ..tool.coin import tool_cmd
|
||||
t = tool_cmd(self.cfg, mmtype=mmtype)
|
||||
addr = t.privhex2addr('bead' * 16)
|
||||
sample_addr = addr.views[addr.view_pref]
|
||||
|
||||
return f"""
|
||||
EXAMPLES:
|
||||
|
||||
Send 0.123 {self.proto.coin} to an external {self.proto.name} address, returning the change to a
|
||||
specific MMGen address in the tracking wallet:
|
||||
|
||||
$ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}:7
|
||||
|
||||
Same as above, but select the change address automatically:
|
||||
|
||||
$ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}
|
||||
|
||||
Same as above, but select the change address automatically by address type:
|
||||
|
||||
$ {gc.prog_name} {sample_addr},0.123 {mmtype}
|
||||
|
||||
Same as above, but reduce verbosity and specify fee of 20 satoshis
|
||||
per byte:
|
||||
|
||||
$ {gc.prog_name} -q -f 20s {sample_addr},0.123 {mmtype}
|
||||
|
||||
Send entire balance of selected inputs minus fee to an external {self.proto.name}
|
||||
address:
|
||||
|
||||
$ {gc.prog_name} {sample_addr}
|
||||
|
||||
Send entire balance of selected inputs minus fee to first unused wallet
|
||||
address of specified type:
|
||||
|
||||
$ {gc.prog_name} {mmtype}
|
||||
"""
|
||||
|
||||
def txcreate(self):
|
||||
return f"""
|
||||
The transaction’s outputs are listed on the command line, while its inputs
|
||||
are chosen from a list of the wallet’s unspent outputs via an interactive
|
||||
menu. Alternatively, inputs may be specified using the --inputs option.
|
||||
|
||||
All addresses on the command line can be either {self.proto.name} addresses or MMGen
|
||||
IDs in the form <seed ID>:<address type letter>:<index>.
|
||||
|
||||
Outputs are specified in the form <address>,<amount>, with the change output
|
||||
specified by address only. Alternatively, the change output may be an
|
||||
addrlist ID in the form <seed ID>:<address type letter>, in which case the
|
||||
first unused address in the tracking wallet matching the requested ID will
|
||||
be automatically selected as the change output.
|
||||
|
||||
If the transaction fee is not specified on the command line (see FEE
|
||||
SPECIFICATION below), it will be calculated dynamically using network fee
|
||||
estimation for the default (or user-specified) number of confirmations.
|
||||
If network fee estimation fails, the user will be prompted for a fee.
|
||||
|
||||
Network-estimated fees will be multiplied by the value of --fee-adjust, if
|
||||
specified.
|
||||
|
||||
To send the value of all inputs (minus TX fee) to a single output, specify
|
||||
a single address with no amount on the command line. Alternatively, an
|
||||
addrlist ID may be specified, and the address will be chosen automatically
|
||||
as described above for the change output.
|
||||
"""
|
||||
|
||||
def txsign(self):
|
||||
from ..proto.btc.params import mainnet
|
||||
return """
|
||||
Transactions may contain both {pnm} or non-{pnm} input addresses.
|
||||
|
||||
To sign non-{pnm} inputs, a {wd}flat key list is used
|
||||
as the key source (--keys-from-file option).
|
||||
|
||||
To sign {pnm} inputs, key data is generated from a seed as with the
|
||||
{pnl}-addrgen and {pnl}-keygen commands. Alternatively, a key-address file
|
||||
may be used (--mmgen-keys-from-file option).
|
||||
|
||||
Multiple wallets or other seed files can be listed on the command line in
|
||||
any order. If the seeds required to sign the transaction’s inputs are not
|
||||
found in these files (or in the default wallet), the user will be prompted
|
||||
for seed data interactively.
|
||||
|
||||
To prevent an attacker from crafting transactions with bogus {pnm}-to-{pnu}
|
||||
address mappings, all outputs to {pnm} addresses are verified with a seed
|
||||
source. Therefore, seed files or a key-address file for all {pnm} outputs
|
||||
must also be supplied on the command line if the data can’t be found in the
|
||||
default wallet.
|
||||
""".format(
|
||||
wd = f'{self.coind_exec()} wallet dump or ' if isinstance(self.proto, mainnet) else '',
|
||||
pnm = gc.proj_name,
|
||||
pnu = self.proto.name,
|
||||
pnl = gc.proj_name.lower())
|
||||
|
||||
def subwallet(self):
|
||||
from ..subseed import SubSeedIdxRange
|
||||
return f"""
|
||||
SUBWALLETS:
|
||||
|
||||
Subwallets (subseeds) are specified by a ‘Subseed Index’ consisting of:
|
||||
|
||||
a) an integer in the range 1-{SubSeedIdxRange.max_idx}, plus
|
||||
b) an optional single letter, ‘L’ or ‘S’
|
||||
|
||||
The letter designates the length of the subseed. If omitted, ‘L’ is assumed.
|
||||
|
||||
Long (‘L’) subseeds are the same length as their parent wallet’s seed
|
||||
(typically 256 bits), while short (‘S’) subseeds are always 128-bit.
|
||||
The long and short subseeds for a given index are derived independently,
|
||||
so both may be used.
|
||||
|
||||
MMGen Wallet has no notion of ‘depth’, and to an outside observer subwallets
|
||||
are identical to ordinary wallets. This is a feature rather than a bug, as
|
||||
it denies an attacker any way of knowing whether a given wallet has a parent.
|
||||
|
||||
Since subwallets are just wallets, they may be used to generate other
|
||||
subwallets, leading to hierarchies of arbitrary depth. However, this is
|
||||
inadvisable in practice for two reasons: Firstly, it creates accounting
|
||||
complexity, requiring the user to independently keep track of a derivation
|
||||
tree. More importantly, however, it leads to the danger of Seed ID
|
||||
collisions between subseeds at different levels of the hierarchy, as
|
||||
MMGen checks and avoids ID collisions only among sibling subseeds.
|
||||
|
||||
An exception to this caveat would be a multi-user setup where sibling
|
||||
subwallets are distributed to different users as their default wallets.
|
||||
Since the subseeds derived from these subwallets are private to each user,
|
||||
Seed ID collisions among them doesn’t present a problem.
|
||||
|
||||
A safe rule of thumb, therefore, is for *each user* to derive all of his/her
|
||||
subwallets from a single parent. This leaves each user with a total of two
|
||||
million subwallets, which should be enough for most practical purposes.
|
||||
""".strip()
|
||||
327
mmgen/opts.py
327
mmgen/opts.py
|
|
@ -17,13 +17,128 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
opts: MMGen-specific command-line options processing after generic processing by share.Opts
|
||||
opts: command-line options processing for the MMGen Project
|
||||
"""
|
||||
import sys,os
|
||||
|
||||
from .share import Opts
|
||||
import sys, os, re
|
||||
from collections import namedtuple
|
||||
|
||||
from .cfg import gc
|
||||
|
||||
def get_opt_by_substring(opt, opts):
|
||||
matches = [o for o in opts if o.startswith(opt)]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
if len(matches) > 1:
|
||||
from .util import die
|
||||
die('CmdlineOptError', f'--{opt}: ambiguous option (not unique substring)')
|
||||
|
||||
def process_uopts(opts_data, opts):
|
||||
|
||||
from .util import die
|
||||
|
||||
def get_uopts():
|
||||
nonlocal uargs
|
||||
idx = 1
|
||||
argv_len = len(sys.argv)
|
||||
while idx < argv_len:
|
||||
arg = sys.argv[idx]
|
||||
if len(arg) > 4096:
|
||||
raise RuntimeError(f'{len(arg)} bytes: command-line argument too long')
|
||||
if arg.startswith('--'):
|
||||
if len(arg) == 2:
|
||||
uargs = sys.argv[idx+1:]
|
||||
return
|
||||
opt, parm = arg[2:].split('=') if '=' in arg else (arg[2:], None)
|
||||
if len(opt) < 2:
|
||||
die('CmdlineOptError', f'--{opt}: option name must be at least two characters long')
|
||||
if opt in opts or (opt := get_opt_by_substring(opt, opts)):
|
||||
if opts[opt].has_parm:
|
||||
if parm:
|
||||
yield (opts[opt].name, parm)
|
||||
else:
|
||||
idx += 1
|
||||
if idx == argv_len or (parm := sys.argv[idx]).startswith('-'):
|
||||
die('CmdlineOptError', f'missing parameter for option --{opt}')
|
||||
yield (opts[opt].name, parm)
|
||||
else:
|
||||
if parm:
|
||||
die('CmdlineOptError', f'option --{opt} requires no parameter')
|
||||
yield (opts[opt].name, True)
|
||||
else:
|
||||
opt, parm = arg[2:].split('=') if '=' in arg else (arg[2:], None)
|
||||
die('CmdlineOptError', f'--{opt}: unrecognized option')
|
||||
elif arg[0] == '-' and len(arg) > 1:
|
||||
for j, sopt in enumerate(arg[1:]):
|
||||
if sopt in opts:
|
||||
if opts[sopt].has_parm:
|
||||
if j > 0:
|
||||
die('CmdlineOptError', f'{arg}: short option with parameters cannot be combined')
|
||||
if arg[2:]:
|
||||
yield (opts[sopt].name, arg[2:])
|
||||
else:
|
||||
idx += 1
|
||||
if idx == argv_len or (parm := sys.argv[idx]).startswith('-'):
|
||||
die('CmdlineOptError', f'missing parameter for option -{sopt}')
|
||||
yield (opts[sopt].name, parm)
|
||||
break
|
||||
else:
|
||||
yield (opts[sopt].name, True)
|
||||
else:
|
||||
die('CmdlineOptError', f'-{sopt}: unrecognized option')
|
||||
else:
|
||||
uargs = sys.argv[idx:]
|
||||
return
|
||||
idx += 1
|
||||
|
||||
uargs = []
|
||||
uopts = dict(get_uopts())
|
||||
|
||||
if 'sets' in opts_data:
|
||||
for a_opt, a_val, b_opt, b_val in opts_data['sets']:
|
||||
if a_opt in uopts:
|
||||
u_val = uopts[a_opt]
|
||||
if (u_val and a_val == bool) or u_val == a_val:
|
||||
if b_opt in uopts and uopts[b_opt] != b_val:
|
||||
die(1,
|
||||
'Option conflict:'
|
||||
+ '\n --{}={}, with'.format(b_opt.replace('_', '-'), uopts[b_opt])
|
||||
+ '\n --{}={}\n'.format(a_opt.replace('_', '-'), uopts[a_opt]))
|
||||
else:
|
||||
uopts[b_opt] = b_val
|
||||
|
||||
return uopts, uargs
|
||||
|
||||
cmd_opts_pat = re.compile(r'^-([a-zA-Z0-9-]), --([a-zA-Z0-9-]{2,64})(=| )(.+)')
|
||||
global_opts_pat = re.compile(r'^\t\t\t(.)(.) --([a-zA-Z0-9-]{2,64})(=| )(.+)')
|
||||
ao = namedtuple('opt', ['name', 'has_parm'])
|
||||
|
||||
def parse_opts(opts_data, opt_filter, global_opts_data, global_opts_filter):
|
||||
|
||||
def parse_cmd_opts_text():
|
||||
for line in opts_data['text']['options'].strip().splitlines():
|
||||
m = cmd_opts_pat.match(line)
|
||||
if m and (not opt_filter or m[1] in opt_filter):
|
||||
ret = ao(m[2].replace('-', '_'), m[3] == '=')
|
||||
yield (m[1], ret)
|
||||
yield (m[2], ret)
|
||||
|
||||
def parse_global_opts_text():
|
||||
for line in global_opts_data['text'].splitlines():
|
||||
m = global_opts_pat.match(line)
|
||||
if m and m[1] in global_opts_filter.coin and m[2] in global_opts_filter.cmd:
|
||||
yield (m[3], ao(m[3].replace('-', '_'), m[4] == '='))
|
||||
|
||||
opts = tuple(parse_cmd_opts_text()) + tuple(parse_global_opts_text())
|
||||
|
||||
uopts, uargs = process_uopts(opts_data, dict(opts))
|
||||
|
||||
return namedtuple('parsed_cmd_opts', ['user_opts', 'cmd_args', 'opts'])(
|
||||
uopts, # dict
|
||||
uargs, # list, callers can pop
|
||||
tuple(v.name for k,v in opts if len(k) > 1)
|
||||
)
|
||||
|
||||
def opt_preproc_debug(po):
|
||||
d = (
|
||||
('Cmdline', ' '.join(sys.argv), False),
|
||||
|
|
@ -37,48 +152,6 @@ def opt_preproc_debug(po):
|
|||
for label,data,pretty in d:
|
||||
Msg(' {:<20}: {}'.format(label,'\n' + fmt_list(data,fmt='col',indent=' '*8) if pretty else data))
|
||||
|
||||
long_opts_data = {
|
||||
'text': """
|
||||
--, --accept-defaults Accept defaults at all prompts
|
||||
--, --coin=c Choose coin unit. Default: BTC. Current choice: {cu_dfl}
|
||||
--, --token=t Specify an ERC20 token by address or symbol
|
||||
--, --cashaddr=0|1 Display BCH addresses in cashaddr format (default: 1)
|
||||
--, --color=0|1 Disable or enable color output (default: 1)
|
||||
--, --columns=N Force N columns of output with certain commands
|
||||
--, --scroll Use the curses-like scrolling interface for
|
||||
tracking wallet views
|
||||
--, --force-256-color Force 256-color output when color is enabled
|
||||
--, --pager Pipe output of certain commands to pager (WIP)
|
||||
--, --data-dir=path Specify {pnm} data directory location
|
||||
--, --daemon-data-dir=path Specify coin daemon data directory location
|
||||
--, --daemon-id=ID Specify the coin daemon ID
|
||||
--, --ignore-daemon-version Ignore coin daemon version check
|
||||
--, --http-timeout=t Set HTTP timeout in seconds for JSON-RPC connections
|
||||
--, --no-license Suppress the GPL license prompt
|
||||
--, --rpc-host=HOST Communicate with coin daemon running on host HOST
|
||||
--, --rpc-port=PORT Communicate with coin daemon listening on port PORT
|
||||
--, --rpc-user=USER Authenticate to coin daemon using username USER
|
||||
--, --rpc-password=PASS Authenticate to coin daemon using password PASS
|
||||
--, --rpc-backend=backend Use backend 'backend' for JSON-RPC communications
|
||||
--, --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
|
||||
--, --regtest=0|1 Disable or enable regtest mode
|
||||
--, --testnet=0|1 Disable or enable testnet
|
||||
--, --tw-name=NAME Specify alternate name for the BTC/LTC/BCH tracking
|
||||
wallet (default: ‘{tw_name}’)
|
||||
--, --skip-cfg-file Skip reading the configuration file
|
||||
--, --usage Print usage information and exit
|
||||
--, --version Print version information and exit
|
||||
--, --bob Specify user “Bob” in MMGen regtest mode
|
||||
--, --alice Specify user “Alice” in MMGen regtest mode
|
||||
--, --carol Specify user “Carol” in MMGen regtest mode
|
||||
""",
|
||||
'code': lambda proto,help_notes,s: s.format(
|
||||
pnm = gc.proj_name,
|
||||
cu_dfl = proto.coin,
|
||||
tw_name = help_notes('dfl_twname')
|
||||
)
|
||||
}
|
||||
|
||||
opts_data_dfl = {
|
||||
'text': {
|
||||
'desc': '',
|
||||
|
|
@ -90,7 +163,22 @@ opts_data_dfl = {
|
|||
}
|
||||
}
|
||||
|
||||
class UserOpts:
|
||||
def get_coin():
|
||||
for n, arg in enumerate(sys.argv[1:]):
|
||||
if len(arg) > 4096:
|
||||
raise RuntimeError(f'{len(arg)} bytes: command-line argument too long')
|
||||
if arg.startswith('--coin='):
|
||||
return arg.removeprefix('--coin=').lower()
|
||||
if arg == '--coin':
|
||||
if len(sys.argv) < n + 3:
|
||||
from .util import die
|
||||
die('CmdlineOptError', f'{arg}: missing parameter')
|
||||
return sys.argv[n + 2].lower()
|
||||
if arg == '-' or not arg.startswith('-'): # stop at first non-option
|
||||
return 'btc'
|
||||
return 'btc'
|
||||
|
||||
class Opts:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -98,16 +186,19 @@ class UserOpts:
|
|||
opts_data,
|
||||
init_opts, # dict containing opts to pre-initialize
|
||||
opt_filter, # whitelist of opt letters; all others are skipped
|
||||
parse_only,
|
||||
parsed_opts):
|
||||
parsed_opts,
|
||||
need_proto):
|
||||
|
||||
self.opts_data = od = opts_data or opts_data_dfl
|
||||
if len(sys.argv) > 257:
|
||||
raise RuntimeError(f'{len(sys.argv) - 1}: too many command-line arguments')
|
||||
|
||||
opts_data = opts_data or opts_data_dfl
|
||||
self.opt_filter = opt_filter
|
||||
|
||||
od['text']['long_options'] = long_opts_data['text']
|
||||
self.global_opts_filter = self.get_global_opts_filter(need_proto)
|
||||
self.opts_data = opts_data
|
||||
|
||||
# po: (user_opts,cmd_args,opts,filtered_opts)
|
||||
po = parsed_opts or Opts.parse_opts(od,opt_filter=opt_filter)
|
||||
po = parsed_opts or parse_opts(opts_data, opt_filter, self.global_opts_data, self.global_opts_filter)
|
||||
|
||||
cfg._args = po.cmd_args
|
||||
cfg._uopts = uopts = po.user_opts
|
||||
|
|
@ -127,60 +218,90 @@ class UserOpts:
|
|||
if os.getenv('MMGEN_DEBUG_OPTS'):
|
||||
opt_preproc_debug(po)
|
||||
|
||||
for funcname in ('usage', 'version', 'show_hash_presets'):
|
||||
for funcname in self.info_funcs:
|
||||
if funcname in uopts:
|
||||
import importlib
|
||||
getattr(importlib.import_module('mmgen.help'), funcname)(cfg) # exits
|
||||
getattr(importlib.import_module(self.help_pkg), funcname)(cfg) # exits
|
||||
|
||||
if parse_only:
|
||||
return
|
||||
class UserOpts(Opts):
|
||||
|
||||
def init_bottom(self,cfg):
|
||||
# print help screen only after globals initialized and locked:
|
||||
if cfg.help or cfg.longhelp:
|
||||
self.print_help(cfg) # exits
|
||||
help_pkg = 'mmgen.help'
|
||||
info_funcs = ('usage', 'version', 'show_hash_presets')
|
||||
|
||||
def usage(self):
|
||||
from .util import Die
|
||||
Die(1,Opts.make_usage_str(gc.prog_name,'user',self.usage_data))
|
||||
global_opts_data = {
|
||||
# coin code : cmd code : opt : opt param : text
|
||||
'text': """
|
||||
-- --accept-defaults Accept defaults at all prompts
|
||||
hp --cashaddr=0|1 Display addresses in cashaddr format (default: 1)
|
||||
-p --coin=c Choose coin unit. Default: BTC. Current choice: {cu_dfl}
|
||||
er --token=t Specify an ERC20 token by address or symbol
|
||||
-- --color=0|1 Disable or enable color output (default: 1)
|
||||
-- --columns=N Force N columns of output with certain commands
|
||||
Rr --scroll Use the curses-like scrolling interface for
|
||||
+ tracking wallet views
|
||||
-- --force-256-color Force 256-color output when color is enabled
|
||||
-- --pager Pipe output of certain commands to pager (WIP)
|
||||
-- --data-dir=path Specify {pnm} data directory location
|
||||
rr --daemon-data-dir=path Specify coin daemon data directory location
|
||||
Rr --daemon-id=ID Specify the coin daemon ID
|
||||
rr --ignore-daemon-version Ignore coin daemon version check
|
||||
rr --http-timeout=t Set HTTP timeout in seconds for JSON-RPC connections
|
||||
-- --no-license Suppress the GPL license prompt
|
||||
rr --rpc-host=HOST Communicate with coin daemon running on host HOST
|
||||
rr --rpc-port=PORT Communicate with coin daemon listening on port PORT
|
||||
rr --rpc-user=USER Authenticate to coin daemon using username USER
|
||||
rr --rpc-password=PASS Authenticate to coin daemon using password PASS
|
||||
Rr --rpc-backend=backend Use backend 'backend' for JSON-RPC communications
|
||||
Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
|
||||
-p --regtest=0|1 Disable or enable regtest mode
|
||||
-- --testnet=0|1 Disable or enable testnet
|
||||
br --tw-name=NAME Specify alternate name for the BTC/LTC/BCH tracking
|
||||
+ wallet (default: ‘{tw_name}’)
|
||||
-- --skip-cfg-file Skip reading the configuration file
|
||||
-- --version Print version information and exit
|
||||
-- --usage Print usage information and exit
|
||||
b- --bob Specify user ‘Bob’ in MMGen regtest mode
|
||||
b- --alice Specify user ‘Alice’ in MMGen regtest mode
|
||||
b- --carol Specify user ‘Carol’ in MMGen regtest mode
|
||||
""",
|
||||
'code': lambda proto, help_notes, s: s.format(
|
||||
pnm = gc.proj_name,
|
||||
cu_dfl = proto.coin,
|
||||
tw_name = help_notes('dfl_twname'))
|
||||
}
|
||||
|
||||
def version(self):
|
||||
from .util import Die,fmt
|
||||
Die(0,fmt(f"""
|
||||
{gc.prog_name.upper()} version {gc.version}
|
||||
Part of {gc.proj_name} Wallet, an online/offline cryptocurrency wallet for the
|
||||
command line. Copyright (C){gc.Cdates} {gc.author} {gc.email}
|
||||
""",indent=' ').rstrip())
|
||||
|
||||
def print_help(self,cfg):
|
||||
|
||||
if not 'code' in self.opts_data:
|
||||
self.opts_data['code'] = {}
|
||||
|
||||
from .protocol import init_proto_from_cfg
|
||||
proto = init_proto_from_cfg(cfg,need_amt=True)
|
||||
|
||||
if getattr(cfg,'longhelp',None):
|
||||
self.opts_data['code']['long_options'] = long_opts_data['code']
|
||||
def remove_unneeded_long_opts():
|
||||
d = self.opts_data['text']['long_options']
|
||||
if proto.base_proto != 'Ethereum':
|
||||
d = '\n'.join(''+i for i in d.split('\n') if not '--token' in i)
|
||||
self.opts_data['text']['long_options'] = d
|
||||
remove_unneeded_long_opts()
|
||||
|
||||
from .ui import do_pager
|
||||
do_pager(Opts.make_help( cfg, proto, self.opts_data, self.opt_filter ))
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
def show_hash_presets(self):
|
||||
fs = ' {:<6} {:<3} {:<2} {}'
|
||||
from .util import msg
|
||||
from .crypto import Crypto
|
||||
msg(' Available parameters for scrypt.hash():')
|
||||
msg(fs.format('Preset','N','r','p'))
|
||||
for i in sorted(Crypto.hash_presets.keys()):
|
||||
msg(fs.format(i,*Crypto.hash_presets[i]))
|
||||
msg(' N = memory usage (power of two)\n p = iterations (rounds)')
|
||||
sys.exit(0)
|
||||
@staticmethod
|
||||
def get_global_opts_filter(need_proto):
|
||||
"""
|
||||
Coin codes:
|
||||
'b' - Bitcoin or Bitcoin code fork supporting RPC
|
||||
'R' - Bitcoin or Ethereum code fork supporting RPC
|
||||
'e' - Ethereum or Ethereum code fork
|
||||
'r' - coin supporting RPC
|
||||
'h' - Bitcoin Cash
|
||||
'-' - other coin
|
||||
Cmd codes:
|
||||
'p' - proto required
|
||||
'r' - RPC required
|
||||
'-' - no capabilities required
|
||||
"""
|
||||
ret = namedtuple('global_opts_filter', ['coin', 'cmd'])
|
||||
if caps := gc.cmd_caps:
|
||||
coin = caps.coin if caps.coin and len(caps.coin) > 1 else get_coin()
|
||||
return ret(
|
||||
coin = (
|
||||
('-', 'r', 'R', 'b', 'h') if coin == 'bch' else
|
||||
('-', 'r', 'R', 'b') if coin in gc.btc_fork_rpc_coins else
|
||||
('-', 'r', 'R', 'e') if coin in gc.eth_fork_coins else
|
||||
('-', 'r') if coin in gc.rpc_coins else
|
||||
('-')),
|
||||
cmd = (
|
||||
['-']
|
||||
+ (['r'] if caps.rpc else [])
|
||||
+ (['p'] if caps.proto else [])
|
||||
))
|
||||
else:
|
||||
return ret(
|
||||
coin = ('-', 'r', 'R', 'b', 'h', 'e'),
|
||||
cmd = ('-', 'r', 'p')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,182 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# Opts.py, an options parsing library for Python.
|
||||
# Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
share.Opts: Generic options parsing
|
||||
"""
|
||||
|
||||
import sys,re
|
||||
from collections import namedtuple
|
||||
|
||||
pat = re.compile(r'^-([a-zA-Z0-9-]), --([a-zA-Z0-9-]{2,64})(=| )(.+)')
|
||||
|
||||
def make_usage_str(prog_name,caller,data):
|
||||
lines = [data.strip()] if isinstance(data,str) else data
|
||||
indent,col1_w = {
|
||||
'help': (2,len(prog_name)+1),
|
||||
'user': (0,len('USAGE:')),
|
||||
}[caller]
|
||||
def gen():
|
||||
ulbl = 'USAGE:'
|
||||
for line in lines:
|
||||
yield f'{ulbl:{col1_w}} {prog_name} {line}'
|
||||
ulbl = ''
|
||||
return ('\n'+(' '*indent)).join(gen())
|
||||
|
||||
def usage(opts_data):
|
||||
print(make_usage_str(
|
||||
prog_name = opts_data['prog_name'],
|
||||
caller = 'user',
|
||||
data = opts_data['text'].get('usage2') or opts_data['text']['usage'] ))
|
||||
sys.exit(1)
|
||||
|
||||
def print_help(*args):
|
||||
print(make_help(*args))
|
||||
sys.exit(0)
|
||||
|
||||
def make_help(cfg,proto,opts_data,opt_filter):
|
||||
|
||||
def parse_lines(text):
|
||||
filtered = False
|
||||
for line in text.strip().splitlines():
|
||||
m = pat.match(line)
|
||||
if m:
|
||||
filtered = bool(opt_filter and m[1] not in opt_filter)
|
||||
if not filtered:
|
||||
yield fs.format( ('-'+m[1]+',','')[m[1]=='-'], m[2], m[4] )
|
||||
elif not filtered:
|
||||
yield line
|
||||
|
||||
opts_type,fs = ('options','{:<3} --{} {}') if cfg.help else ('long_options','{} --{} {}')
|
||||
t = opts_data['text']
|
||||
c = opts_data['code']
|
||||
nl = '\n '
|
||||
|
||||
pn = opts_data['prog_name']
|
||||
|
||||
from ..help import help_notes_func
|
||||
def help_notes(k):
|
||||
return help_notes_func(proto,cfg,k)
|
||||
|
||||
def help_mod(modname):
|
||||
import importlib
|
||||
return importlib.import_module('mmgen.help.'+modname).help(proto,cfg)
|
||||
|
||||
def gen_arg_tuple(func,text):
|
||||
d = {
|
||||
'proto': proto,
|
||||
'help_notes': help_notes,
|
||||
'help_mod': help_mod,
|
||||
'cfg': cfg,
|
||||
}
|
||||
for arg in func.__code__.co_varnames:
|
||||
yield d[arg] if arg in d else text
|
||||
|
||||
def gen_text():
|
||||
yield ' {} {}'.format(pn.upper()+':',t['desc'].strip())
|
||||
yield make_usage_str(pn,'help',t.get('usage2') or t['usage'])
|
||||
yield opts_type.upper().replace('_',' ') + ':'
|
||||
|
||||
# process code for options
|
||||
opts_text = nl.join(parse_lines(t[opts_type]))
|
||||
if opts_type in c:
|
||||
arg_tuple = tuple(gen_arg_tuple(c[opts_type],opts_text))
|
||||
yield c[opts_type](*arg_tuple)
|
||||
else:
|
||||
yield opts_text
|
||||
|
||||
# process code for notes
|
||||
if opts_type == 'options' and 'notes' in t:
|
||||
notes_text = t['notes']
|
||||
if 'notes' in c:
|
||||
arg_tuple = tuple(gen_arg_tuple(c['notes'],notes_text))
|
||||
notes_text = c['notes'](*arg_tuple)
|
||||
yield from notes_text.splitlines()
|
||||
|
||||
return nl.join(gen_text()) + '\n'
|
||||
|
||||
def process_uopts(opts_data,short_opts,long_opts):
|
||||
|
||||
import os,getopt
|
||||
opts_data['prog_name'] = os.path.basename(sys.argv[0])
|
||||
|
||||
try:
|
||||
cl_uopts,uargs = getopt.getopt(sys.argv[1:],''.join(short_opts),long_opts)
|
||||
except getopt.GetoptError as e:
|
||||
print(e.args[0])
|
||||
sys.exit(1)
|
||||
|
||||
def get_uopts():
|
||||
for uopt,uparm in cl_uopts:
|
||||
if uopt.startswith('--'):
|
||||
lo = uopt[2:]
|
||||
if lo in long_opts:
|
||||
yield (lo.replace('-','_'), True)
|
||||
else: # lo+'=' in long_opts
|
||||
yield (lo.replace('-','_'), uparm)
|
||||
else: # uopt.startswith('-')
|
||||
so = uopt[1]
|
||||
if so in short_opts:
|
||||
yield (long_opts[short_opts.index(so)].replace('-','_'), True)
|
||||
else: # so+':' in short_opts
|
||||
yield (long_opts[short_opts.index(so+':')][:-1].replace('-','_'), uparm)
|
||||
|
||||
uopts = dict(get_uopts())
|
||||
|
||||
if 'sets' in opts_data:
|
||||
for a_opt,a_val,b_opt,b_val in opts_data['sets']:
|
||||
if a_opt in uopts:
|
||||
u_val = uopts[a_opt]
|
||||
if (u_val and a_val == bool) or u_val == a_val:
|
||||
if b_opt in uopts and uopts[b_opt] != b_val:
|
||||
sys.stderr.write(
|
||||
'Option conflict:'
|
||||
+ '\n --{}={}, with'.format(b_opt.replace('_','-'),uopts[b_opt])
|
||||
+ '\n --{}={}\n'.format(a_opt.replace('_','-'),uopts[a_opt]) )
|
||||
sys.exit(1)
|
||||
else:
|
||||
uopts[b_opt] = b_val
|
||||
|
||||
return uopts,uargs
|
||||
|
||||
def parse_opts(opts_data,opt_filter=None):
|
||||
|
||||
short_opts,long_opts,filtered_opts = [],[],[]
|
||||
def parse_lines(opts_type):
|
||||
for line in opts_data['text'][opts_type].strip().splitlines():
|
||||
m = pat.match(line)
|
||||
if m:
|
||||
if opt_filter and m[1] not in opt_filter:
|
||||
filtered_opts.append(m[2])
|
||||
else:
|
||||
if opts_type == 'options':
|
||||
short_opts.append(m[1] + ('',':')[m[3] == '='])
|
||||
long_opts.append(m[2] + ('','=')[m[3] == '='])
|
||||
|
||||
parse_lines('options')
|
||||
if 'long_options' in opts_data['text']:
|
||||
parse_lines('long_options')
|
||||
|
||||
uopts,uargs = process_uopts(opts_data,short_opts,long_opts)
|
||||
|
||||
return namedtuple('parsed_cmd_opts',['user_opts','cmd_args','opts','filtered_opts'])(
|
||||
uopts, # dict
|
||||
uargs, # list, callers can pop
|
||||
tuple(o.replace('-','_').rstrip('=') for o in long_opts),
|
||||
tuple(o.replace('-','_') for o in filtered_opts),
|
||||
)
|
||||
|
|
@ -82,4 +82,6 @@ ignored-classes = [ # ignored for no-member, otherwise checked
|
|||
"GenTool",
|
||||
"VirtBlockDeviceBase",
|
||||
"SwapMgrBase",
|
||||
"Opts",
|
||||
"Help",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ packages =
|
|||
mmgen.proto.secp256k1
|
||||
mmgen.proto.xmr
|
||||
mmgen.proto.zec
|
||||
mmgen.share
|
||||
mmgen.tool
|
||||
mmgen.tx
|
||||
mmgen.tw
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class CmdTestHelp(CmdTestBase):
|
|||
return t
|
||||
|
||||
def longhelpscreens(self):
|
||||
return self.helpscreens(arg='--longhelp',expect='USAGE:.*LONG OPTIONS:')
|
||||
return self.helpscreens(arg='--longhelp',expect='USAGE:.*GLOBAL OPTIONS:')
|
||||
|
||||
def show_hash_presets(self):
|
||||
return self.helpscreens(
|
||||
|
|
|
|||
|
|
@ -16,17 +16,44 @@ from ..include.common import cfg
|
|||
from .ct_base import CmdTestBase
|
||||
|
||||
class CmdTestOpts(CmdTestBase):
|
||||
'options processing'
|
||||
'command-line options parsing and processing'
|
||||
networks = ('btc',)
|
||||
tmpdir_nums = [41]
|
||||
cmd_group = (
|
||||
('opt_helpscreen', (41,"helpscreen output", [])),
|
||||
('opt_noargs', (41,"invocation with no user options or arguments", [])),
|
||||
('opt_good', (41,"good opts", [])),
|
||||
('opt_bad_infile', (41,"bad infile parameter", [])),
|
||||
('opt_bad_outdir', (41,"bad outdir parameter", [])),
|
||||
('opt_bad_incompatible', (41,"incompatible opts", [])),
|
||||
('opt_bad_autoset', (41,"invalid autoset value", [])),
|
||||
('opt_helpscreen', (41, 'helpscreen output', [])),
|
||||
('opt_noargs', (41, 'invocation with no user options or arguments', [])),
|
||||
('opt_good1', (41, 'good opts (long opts only)', [])),
|
||||
('opt_good2', (41, 'good opts (mixed short and long opts)', [])),
|
||||
('opt_good3', (41, 'good opts (max arg count)', [])),
|
||||
('opt_good4', (41, 'good opts (maxlen arg)', [])),
|
||||
('opt_good5', (41, 'good opts (long opt substring)', [])),
|
||||
('opt_good6', (41, 'good global opt (--coin=xmr)', [])),
|
||||
('opt_good7', (41, 'good global opt (--coin xmr)', [])),
|
||||
('opt_good8', (41, 'good global opt (--pager)', [])),
|
||||
('opt_good9', (41, 'good cmdline arg ‘-’', [])),
|
||||
('opt_good10', (41, 'good cmdline arg ‘-’ with arg', [])),
|
||||
('opt_good11', (41, 'good cmdline arg ‘-’ with option', [])),
|
||||
('opt_bad_param', (41, 'bad global opt (--pager=1)', [])),
|
||||
('opt_bad_infile', (41, 'bad infile parameter', [])),
|
||||
('opt_bad_outdir', (41, 'bad outdir parameter', [])),
|
||||
('opt_bad_incompatible', (41, 'incompatible opts', [])),
|
||||
('opt_bad_autoset', (41, 'invalid autoset value', [])),
|
||||
('opt_invalid_1', (41, 'invalid cmdline opt ‘--x’', [])),
|
||||
('opt_invalid_2', (41, 'invalid cmdline opt ‘---’', [])),
|
||||
('opt_invalid_3', (41, 'invalid cmdline opt (combined short opt with param)', [])),
|
||||
('opt_invalid_4', (41, 'invalid cmdline opt (combined short opt with param)', [])),
|
||||
('opt_invalid_5', (41, 'invalid cmdline opt (missing parameter)', [])),
|
||||
('opt_invalid_6', (41, 'invalid cmdline opt (missing parameter)', [])),
|
||||
('opt_invalid_7', (41, 'invalid cmdline opt (parameter not required)', [])),
|
||||
('opt_invalid_8', (41, 'invalid cmdline opt (non-existent option)', [])),
|
||||
('opt_invalid_9', (41, 'invalid cmdline opt (non-existent option)', [])),
|
||||
('opt_invalid_10', (41, 'invalid cmdline opt (missing parameter)', [])),
|
||||
('opt_invalid_11', (41, 'invalid cmdline opt (missing parameter)', [])),
|
||||
('opt_invalid_12', (41, 'invalid cmdline opt (non-existent option)', [])),
|
||||
('opt_invalid_13', (41, 'invalid cmdline opt (ambiguous long opt substring)', [])),
|
||||
('opt_invalid_14', (41, 'invalid cmdline opt (long opt substring too short)', [])),
|
||||
('opt_invalid_15', (41, 'invalid cmdline (too many args)', [])),
|
||||
('opt_invalid_16', (41, 'invalid cmdline (overlong arg)', [])),
|
||||
)
|
||||
|
||||
def spawn_prog(self, args, exit_val=None):
|
||||
|
|
@ -65,10 +92,12 @@ class CmdTestOpts(CmdTestBase):
|
|||
('cfg.outdir', ''), # check_outdir()
|
||||
('cfg.cached_balances', 'False'),
|
||||
('cfg.minconf', '1'),
|
||||
('cfg.coin', 'BTC'),
|
||||
('cfg.pager', 'False'),
|
||||
('cfg.fee_estimate_mode', 'conservative'), # _autoset_opts
|
||||
))
|
||||
|
||||
def opt_good(self):
|
||||
def opt_good1(self):
|
||||
pf_base = 'testfile'
|
||||
pf = os.path.join(self.tmpdir,pf_base)
|
||||
self.write_to_tmpfile(pf_base,'')
|
||||
|
|
@ -90,6 +119,68 @@ class CmdTestOpts(CmdTestBase):
|
|||
('cfg.fee_estimate_mode', 'economical'),
|
||||
))
|
||||
|
||||
def opt_good2(self):
|
||||
return self.check_vals(
|
||||
[
|
||||
'--print-checksum',
|
||||
'-qX',
|
||||
f'--outdir={self.tmpdir}',
|
||||
'-p5',
|
||||
'-m', '0',
|
||||
'--seed-len=256',
|
||||
'-L--my-label',
|
||||
'--seed-len', '128',
|
||||
'--min-temp=-30',
|
||||
'-T-10',
|
||||
'--',
|
||||
'x', 'y', '12345'
|
||||
], (
|
||||
('cfg.print_checksum', 'True'),
|
||||
('cfg.quiet', 'True'),
|
||||
('cfg.outdir', self.tmpdir),
|
||||
('cfg.cached_balances', 'True'),
|
||||
('cfg.minconf', '0'),
|
||||
('cfg.keep_label', 'None'),
|
||||
('cfg.seed_len', '128'),
|
||||
('cfg.hash_preset', '5'),
|
||||
('cfg.label', '--my-label'),
|
||||
('cfg.min_temp', '-30'),
|
||||
('cfg.max_temp', '-10'),
|
||||
('arg1', 'x'),
|
||||
('arg2', 'y'),
|
||||
('arg3', '12345'),
|
||||
))
|
||||
|
||||
def opt_good3(self):
|
||||
return self.check_vals(['m'] * 256, (('arg256', 'm'),))
|
||||
|
||||
def opt_good4(self):
|
||||
return self.check_vals(['e' * 4096], (('arg1', 'e' * 4096),))
|
||||
|
||||
def opt_good5(self):
|
||||
return self.check_vals(['--minc=7'], (('cfg.minconf', '7'),))
|
||||
|
||||
def opt_good6(self):
|
||||
return self.check_vals(['--coin=xmr'], (('cfg.coin', 'XMR'),))
|
||||
|
||||
def opt_good7(self):
|
||||
return self.check_vals(['--coin', 'xmr'], (('cfg.coin', 'XMR'),))
|
||||
|
||||
def opt_good8(self):
|
||||
return self.check_vals(['--pager'], (('cfg.pager', 'True'),))
|
||||
|
||||
def opt_good9(self):
|
||||
return self.check_vals(['-'], (('arg1', '-'),))
|
||||
|
||||
def opt_good10(self):
|
||||
return self.check_vals(['-', '-x'], (('arg1', '-'), ('arg2', '-x')))
|
||||
|
||||
def opt_good11(self):
|
||||
return self.check_vals(['-q', '-', '-x'], (('arg1', '-'), ('arg2', '-x')))
|
||||
|
||||
def opt_bad_param(self):
|
||||
return self.do_run(['--pager=1'], 'no parameter', 1)
|
||||
|
||||
def opt_bad_infile(self):
|
||||
pf = os.path.join(self.tmpdir,'fubar')
|
||||
return self.do_run(['--passwd-file='+pf],'not found',1)
|
||||
|
|
@ -103,3 +194,56 @@ class CmdTestOpts(CmdTestBase):
|
|||
|
||||
def opt_bad_autoset(self):
|
||||
return self.do_run(['--fee-estimate-mode=Fubar'],'not unique substring',1)
|
||||
|
||||
def opt_invalid(self, args, expect, exit_val):
|
||||
t = self.spawn_prog(args, exit_val=exit_val)
|
||||
t.expect(expect)
|
||||
return t
|
||||
|
||||
def opt_invalid_1(self):
|
||||
return self.opt_invalid(['--x'], 'must be at least', 1)
|
||||
|
||||
def opt_invalid_2(self):
|
||||
return self.opt_invalid(['---'], 'must be at least', 1)
|
||||
|
||||
def opt_invalid_3(self):
|
||||
return self.opt_invalid(['-kl3'], 'short option with parameters', 1)
|
||||
|
||||
def opt_invalid_4(self):
|
||||
return self.opt_invalid(['-kl 3'], 'short option with parameters', 1)
|
||||
|
||||
def opt_invalid_5(self):
|
||||
return self.opt_invalid(['-l'], 'missing parameter', 1)
|
||||
|
||||
def opt_invalid_6(self):
|
||||
return self.opt_invalid(['-l', '-k'], 'missing parameter', 1)
|
||||
|
||||
def opt_invalid_7(self):
|
||||
return self.opt_invalid(['--quiet=1'], 'requires no parameter', 1)
|
||||
|
||||
def opt_invalid_8(self):
|
||||
return self.opt_invalid(['-x'], 'unrecognized option', 1)
|
||||
|
||||
def opt_invalid_9(self):
|
||||
return self.opt_invalid(['--frobnicate'], 'unrecognized option', 1)
|
||||
|
||||
def opt_invalid_10(self):
|
||||
return self.opt_invalid(['--label', '-q'], 'missing parameter', 1)
|
||||
|
||||
def opt_invalid_11(self):
|
||||
return self.opt_invalid(['-T', '-10'], 'missing parameter', 1)
|
||||
|
||||
def opt_invalid_12(self):
|
||||
return self.opt_invalid(['-q', '-10'], 'unrecognized option', 1)
|
||||
|
||||
def opt_invalid_13(self):
|
||||
return self.opt_invalid(['--mi=3'], 'ambiguous option', 1)
|
||||
|
||||
def opt_invalid_14(self):
|
||||
return self.opt_invalid(['--m=3'], 'must be at least', 1)
|
||||
|
||||
def opt_invalid_15(self):
|
||||
return self.opt_invalid(['m'] * 257, 'too many', 1)
|
||||
|
||||
def opt_invalid_16(self):
|
||||
return self.opt_invalid(['e' * 4097], 'too long', 1)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ opts_data = {
|
|||
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
|
||||
-P, --passwd-file= f Get wallet passphrase from file 'f'
|
||||
-q, --quiet Be quieter
|
||||
-t, --min-temp= t Minimum temperature (in degrees Celsius)
|
||||
-T, --max-temp= t Maximum temperature (in degrees Celsius)
|
||||
-X, --cached-balances Use cached balances (Ethereum only)
|
||||
-v, --verbose Be more verbose
|
||||
sample help_note: {kgs}
|
||||
|
|
@ -57,6 +59,14 @@ for k in (
|
|||
'cached_balances', # opt_sets_global
|
||||
'minconf', # global_sets_opt
|
||||
'hidden_incog_input_params',
|
||||
'keep_label',
|
||||
'seed_len',
|
||||
'hash_preset',
|
||||
'label',
|
||||
'min_temp',
|
||||
'max_temp',
|
||||
'coin',
|
||||
'pager',
|
||||
):
|
||||
msg('{:30} {}'.format( f'cfg.{k}:', getattr(cfg,k) ))
|
||||
|
||||
|
|
@ -72,3 +82,7 @@ for k in (
|
|||
'fee_estimate_mode', # _autoset_opts
|
||||
):
|
||||
msg('{:30} {}'.format( f'cfg.{k}:', getattr(cfg,k) ))
|
||||
|
||||
msg('')
|
||||
for n, k in enumerate(cfg._args, 1):
|
||||
msg(f'arg{n}: {k}')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue