123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- #!/usr/bin/env python3
- #
- # MMGen Wallet, a terminal-based cryptocurrency wallet
- # Copyright (C)2013-2025 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/>.
- """
- addrlist: Address list classes for the MMGen suite
- """
- from .util import suf, make_chksum_N, Msg, die
- from .objmethods import MMGenObject, HiliteStr, InitErrors
- from .obj import MMGenListItem, ListItemAttr, MMGenDict, TwComment, WalletPassword
- from .key import PrivKey
- from .addr import MMGenID, MMGenAddrType, CoinAddr, AddrIdx, AddrListID, ViewKey
- class AddrIdxList(tuple, InitErrors, MMGenObject):
- max_len = 1000000
- def __new__(cls, *, fmt_str=None, idx_list=None, sep=','):
- try:
- if fmt_str:
- def gen():
- for i in (fmt_str.split(sep)):
- j = [int(x) for x in i.split('-')]
- if len(j) == 1:
- yield j[0]
- elif len(j) == 2:
- if j[0] > j[1]:
- raise ValueError(f'{i}: invalid range')
- yield from range(j[0], j[1] + 1)
- else:
- raise ValueError(f'{i}: invalid range')
- idx_list = tuple(gen())
- return tuple.__new__(cls, sorted({AddrIdx(i) for i in (idx_list or [])}))
- except Exception as e:
- return cls.init_fail(e, idx_list or fmt_str)
- @property
- def id_str(self):
- def gen():
- i_save = self[0]
- yield f'{i_save}'
- in_range = False
- for i in self[1:]:
- if i - i_save == 1:
- in_range = True
- else:
- if in_range:
- in_range = False
- yield f'-{i_save}'
- yield f',{i}'
- i_save = i
- if in_range:
- yield f'-{i_save}'
- return ''.join(gen()) if self else ''
- class AddrListEntryBase(MMGenListItem):
- invalid_attrs = {'proto'}
- def __init__(self, proto, **kwargs):
- self.__dict__['proto'] = proto
- MMGenListItem.__init__(self, **kwargs)
- class AddrListEntry(AddrListEntryBase):
- addr = ListItemAttr(CoinAddr, include_proto=True)
- addr_p2pkh = ListItemAttr(CoinAddr, include_proto=True)
- idx = ListItemAttr(AddrIdx) # not present in flat addrlists
- comment = ListItemAttr(TwComment, reassign_ok=True)
- sec = ListItemAttr(PrivKey, include_proto=True)
- viewkey = ListItemAttr(ViewKey, include_proto=True)
- wallet_passwd = ListItemAttr(WalletPassword)
- class AddrListChksum(HiliteStr):
- color = 'pink'
- trunc_ok = False
- def __new__(cls, addrlist):
- ea = addrlist.al_id.mmtype.extra_attrs or () # add viewkey and passwd to the mix, if present
- lines = [' '.join(
- addrlist.chksum_rec_f(e) +
- tuple(getattr(e, a) for a in ea if getattr(e, a))
- ) for e in addrlist.data]
- return str.__new__(cls, make_chksum_N(' '.join(lines), nchars=16, sep=True))
- class AddrListIDStr(HiliteStr):
- color = 'green'
- trunc_ok = False
- def __new__(cls, addrlist, *, fmt_str=None):
- idxs = [e.idx for e in addrlist.data]
- prev = idxs[0]
- ret = [prev]
- for i in idxs[1:]:
- if i == prev + 1:
- if i == idxs[-1]:
- ret.extend(['-', i])
- else:
- if prev != ret[-1]:
- ret.extend(['-', prev])
- ret.extend([',', i])
- prev = i
- s = ''.join(map(str, ret))
- if fmt_str:
- ret = fmt_str.format(s)
- else:
- proto = addrlist.proto
- coin = 'BTC' if proto.coin == 'BCH' and not addrlist.cfg.cashaddr else proto.coin
- mmtype = addrlist.al_id.mmtype
- ret = '{}{}{}[{}]'.format(
- addrlist.al_id.sid,
- (f'-{coin}', '')[coin == 'BTC'],
- (f'-{mmtype}', '')[mmtype in ('L', 'E')],
- s)
- addrlist.dmsg_sc('id_str', ret[8:].split('[')[0])
- return str.__new__(cls, ret)
- class AddrListData(list, MMGenObject):
- pass
- class AddrList(MMGenObject): # Address info for a single seed ID
- entry_type = AddrListEntry
- main_attr = 'addr'
- desc = 'address'
- gen_desc = 'address'
- gen_desc_pl = 'es'
- gen_addrs = True
- gen_passwds = False
- gen_keys = False
- has_keys = False
- chksum_rec_f = lambda foo, e: (str(e.idx), e.addr.views[e.addr.view_pref])
- def dmsg_sc(self, desc, data):
- Msg(f'sc_debug_{desc}: {data}')
- def noop(self, desc, data):
- pass
- def __init__(
- self,
- cfg,
- proto,
- *,
- infile = '',
- al_id = '',
- adata = [],
- seed = '',
- addr_idxs = '',
- src = '',
- addrlist = '',
- keylist = '',
- mmtype = None,
- key_address_validity_check = None, # None=prompt user, True=check without prompt, False=skip check
- skip_chksum = False,
- skip_chksum_msg = False,
- add_p2pkh = False):
- self.cfg = cfg
- self.ka_validity_chk = key_address_validity_check
- self.add_p2pkh = add_p2pkh
- self.proto = proto
- do_chksum = False
- if not cfg.debug_addrlist:
- self.dmsg_sc = self.noop
- if seed and addr_idxs: # data from seed + idxs
- self.al_id = AddrListID(sid=seed.sid, mmtype=MMGenAddrType(proto, mmtype or proto.dfl_mmtype))
- src = 'gen'
- adata = self.generate(
- seed,
- addr_idxs if isinstance(addr_idxs, AddrIdxList) else AddrIdxList(fmt_str=addr_idxs))
- do_chksum = True
- elif infile: # data from MMGen address file
- self.infile = infile
- adata = self.file.parse_file(infile) # sets self.al_id
- do_chksum = True
- elif al_id and adata: # data from tracking wallet
- self.al_id = al_id
- elif addrlist: # data from flat address list
- self.al_id = None
- from .util import remove_dups
- addrlist = remove_dups(addrlist, edesc='address', desc='address list')
- adata = AddrListData([AddrListEntry(proto=proto, addr=a) for a in addrlist])
- elif keylist: # data from flat key list
- self.al_id = None
- from .util import remove_dups
- keylist = remove_dups(keylist, edesc='key', desc='key list', hide=True)
- adata = AddrListData([AddrListEntry(proto=proto, sec=PrivKey(proto=proto, wif=k)) for k in keylist])
- elif seed or addr_idxs:
- die(3, 'Must specify both seed and addr indexes')
- elif al_id or adata:
- die(3, 'Must specify both al_id and adata')
- else:
- die(3, f'Incorrect arguments for {type(self).__name__}')
- # al_id, adata now set
- self.data = adata
- self.num_addrs = len(adata)
- self.fmt_data = ''
- self.chksum = None
- if self.al_id is None:
- return
- if type(self) is ViewKeyAddrList:
- if not 'viewkey' in self.al_id.mmtype.extra_attrs:
- die(1, f'viewkeys not supported for address type {self.al_id.mmtype.desc!r}')
- self.id_str = AddrListIDStr(self)
- if type(self) is KeyList:
- return
- if do_chksum and not skip_chksum:
- self.chksum = AddrListChksum(self)
- if not skip_chksum_msg:
- self.do_chksum_msg(record=src=='gen')
- def do_chksum_msg(self, record):
- chk = 'Check this value against your records'
- rec = f'Record this checksum: it will be used to verify the {self.desc} file in the future'
- self.cfg._util.qmsg(
- f'Checksum for {self.desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' +
- (chk, rec)[record])
- def generate(self, seed, addr_idxs):
- seed = self.scramble_seed(seed.data)
- self.dmsg_sc('seed', seed[:8].hex())
- mmtype = self.al_id.mmtype
- gen_wallet_passwd = type(self) in (KeyAddrList, ViewKeyAddrList) and 'wallet_passwd' in mmtype.extra_attrs
- gen_viewkey = type(self) in (KeyAddrList, ViewKeyAddrList) and 'viewkey' in mmtype.extra_attrs
- if self.gen_addrs:
- from .keygen import KeyGenerator
- from .addrgen import AddrGenerator
- kg = KeyGenerator(self.cfg, self.proto, mmtype.pubkey_type)
- ag = AddrGenerator(self.cfg, self.proto, mmtype)
- if self.add_p2pkh:
- ag2 = AddrGenerator(self.cfg, self.proto, 'compressed')
- from .derive import derive_coin_privkey_bytes
- t_addrs = len(addr_idxs)
- le = self.entry_type
- out = AddrListData()
- CR = '\n' if self.cfg.debug_addrlist else '\r'
- for pk_bytes in derive_coin_privkey_bytes(seed, addr_idxs):
- if not self.cfg.debug:
- self.cfg._util.qmsg_r(
- f'{CR}Generating {self.gen_desc} #{pk_bytes.idx} ({pk_bytes.pos} of {t_addrs})')
- e = le(proto=self.proto, idx=pk_bytes.idx)
- e.sec = PrivKey(
- self.proto,
- pk_bytes.data,
- compressed = mmtype.compressed,
- pubkey_type = mmtype.pubkey_type)
- if self.gen_addrs:
- data = kg.gen_data(e.sec)
- e.addr = ag.to_addr(data)
- if self.add_p2pkh:
- e.addr_p2pkh = ag2.to_addr(data)
- if gen_viewkey:
- e.viewkey = ag.to_viewkey(data)
- if gen_wallet_passwd:
- e.wallet_passwd = self.gen_wallet_passwd(
- e.viewkey.encode() if type(self) is ViewKeyAddrList else e.sec)
- elif self.gen_passwds:
- e.passwd = self.gen_passwd(e.sec) # TODO - own type
- out.append(e)
- self.cfg._util.qmsg('{}{}: {} {}{} generated{}'.format(
- CR,
- self.al_id.hl(),
- t_addrs,
- self.gen_desc,
- suf(t_addrs, self.gen_desc_pl),
- ' ' * 15))
- return out
- def gen_wallet_passwd(self, privbytes):
- from .proto.btc.common import hash256
- return WalletPassword(hash256(privbytes)[:16].hex())
- def check_format(self, addr):
- return True # format is checked when added to list entry object
- def scramble_seed(self, seed):
- is_btcfork = self.proto.base_coin == 'BTC'
- if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
- self.dmsg_sc('str', '(none)')
- return seed
- if self.proto.base_coin == 'ETH':
- scramble_key = self.proto.coin.lower()
- else:
- scramble_key = (self.proto.coin.lower()+':', '')[is_btcfork] + self.al_id.mmtype.name
- from .crypto import Crypto
- if self.proto.testnet:
- scramble_key += ':' + self.proto.network
- self.dmsg_sc('str', scramble_key)
- return Crypto(self.cfg).scramble_seed(seed, scramble_key.encode())
- def idxs(self):
- return [e.idx for e in self.data]
- def addrs(self):
- return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
- def addrpairs(self):
- return [(e.idx, e.addr) for e in self.data]
- def coinaddrs(self):
- return [e.addr for e in self.data]
- def comments(self):
- return [e.comment for e in self.data]
- def entry(self, idx):
- for e in self.data:
- if idx == e.idx:
- return e
- def coinaddr(self, idx):
- for e in self.data:
- if idx == e.idx:
- return e.addr
- def comment(self, idx):
- for e in self.data:
- if idx == e.idx:
- return e.comment
- def set_comment(self, idx, comment):
- for e in self.data:
- if idx == e.idx:
- e.comment = comment
- def make_reverse_dict_addrlist(self, coinaddrs):
- d = MMGenDict()
- b = coinaddrs
- for e in self.data:
- try:
- d[b[b.index(e.addr)]] = (MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.comment)
- except ValueError:
- pass
- return d
- def add_wifs(self, key_list):
- """
- Match WIF keys in a flat list to addresses in self by generating all
- possible addresses for each key.
- """
- def gen_addr(pk, t):
- at = self.proto.addr_type(t)
- from .keygen import KeyGenerator
- from .addrgen import AddrGenerator
- kg = KeyGenerator(self.cfg, self.proto, at.pubkey_type)
- ag = AddrGenerator(self.cfg, self.proto, at)
- return ag.to_addr(kg.gen_data(pk))
- compressed_types = set(self.proto.mmtypes) - {'L', 'E'}
- uncompressed_types = set(self.proto.mmtypes) & {'L', 'E'}
- def gen():
- for wif in key_list:
- pk = PrivKey(proto=self.proto, wif=wif)
- for t in (compressed_types if pk.compressed else uncompressed_types):
- yield (gen_addr(pk, t), pk)
- addrs4keys = dict(gen())
- for d in self.data:
- if d.addr in addrs4keys:
- d.sec = addrs4keys[d.addr]
- def list_missing(self, attr):
- return [d.addr for d in self.data if not getattr(d, attr)]
- @property
- def file(self):
- if not hasattr(self, '_file'):
- from . import addrfile
- self._file = getattr(addrfile, type(self).__name__.replace('List', 'File'))(self)
- return self._file
- class KeyAddrList(AddrList):
- desc = 'key-address'
- gen_desc = 'key/address pair'
- gen_desc_pl = 's'
- gen_keys = True
- has_keys = True
- chksum_rec_f = lambda foo, e: (str(e.idx), e.addr.views[e.addr.view_pref], e.sec.wif)
- class ViewKeyAddrList(KeyAddrList):
- desc = 'viewkey-address'
- gen_desc = 'viewkey/address pair'
- chksum_rec_f = lambda foo, e: (str(e.idx), e.addr)
- class KeyList(KeyAddrList):
- desc = 'key'
- gen_desc = 'key'
- gen_addrs = False
|