addrlist.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. addrlist: Address list classes for the MMGen suite
  20. """
  21. from .util import suf, make_chksum_N, Msg, die
  22. from .objmethods import MMGenObject, HiliteStr, InitErrors
  23. from .obj import MMGenListItem, ListItemAttr, MMGenDict, TwComment, WalletPassword
  24. from .key import PrivKey
  25. from .addr import MMGenID, MMGenAddrType, CoinAddr, AddrIdx, AddrListID, ViewKey
  26. class AddrIdxList(tuple, InitErrors, MMGenObject):
  27. max_len = 1000000
  28. def __new__(cls, *, fmt_str=None, idx_list=None, sep=','):
  29. try:
  30. if fmt_str:
  31. def gen():
  32. for i in (fmt_str.split(sep)):
  33. j = [int(x) for x in i.split('-')]
  34. if len(j) == 1:
  35. yield j[0]
  36. elif len(j) == 2:
  37. if j[0] > j[1]:
  38. raise ValueError(f'{i}: invalid range')
  39. yield from range(j[0], j[1] + 1)
  40. else:
  41. raise ValueError(f'{i}: invalid range')
  42. idx_list = tuple(gen())
  43. return tuple.__new__(cls, sorted({AddrIdx(i) for i in (idx_list or [])}))
  44. except Exception as e:
  45. return cls.init_fail(e, idx_list or fmt_str)
  46. @property
  47. def id_str(self):
  48. def gen():
  49. i_save = self[0]
  50. yield f'{i_save}'
  51. in_range = False
  52. for i in self[1:]:
  53. if i - i_save == 1:
  54. in_range = True
  55. else:
  56. if in_range:
  57. in_range = False
  58. yield f'-{i_save}'
  59. yield f',{i}'
  60. i_save = i
  61. if in_range:
  62. yield f'-{i_save}'
  63. return ''.join(gen()) if self else ''
  64. class AddrListEntryBase(MMGenListItem):
  65. invalid_attrs = {'proto'}
  66. def __init__(self, proto, **kwargs):
  67. self.__dict__['proto'] = proto
  68. MMGenListItem.__init__(self, **kwargs)
  69. class AddrListEntry(AddrListEntryBase):
  70. addr = ListItemAttr(CoinAddr, include_proto=True)
  71. addr_p2pkh = ListItemAttr(CoinAddr, include_proto=True)
  72. idx = ListItemAttr(AddrIdx) # not present in flat addrlists
  73. comment = ListItemAttr(TwComment, reassign_ok=True)
  74. sec = ListItemAttr(PrivKey, include_proto=True)
  75. viewkey = ListItemAttr(ViewKey, include_proto=True)
  76. wallet_passwd = ListItemAttr(WalletPassword)
  77. class AddrListChksum(HiliteStr):
  78. color = 'pink'
  79. trunc_ok = False
  80. def __new__(cls, addrlist):
  81. ea = addrlist.al_id.mmtype.extra_attrs or () # add viewkey and passwd to the mix, if present
  82. lines = [' '.join(
  83. addrlist.chksum_rec_f(e) +
  84. tuple(getattr(e, a) for a in ea if getattr(e, a))
  85. ) for e in addrlist.data]
  86. return str.__new__(cls, make_chksum_N(' '.join(lines), nchars=16, sep=True))
  87. class AddrListIDStr(HiliteStr):
  88. color = 'green'
  89. trunc_ok = False
  90. def __new__(cls, addrlist, *, fmt_str=None):
  91. idxs = [e.idx for e in addrlist.data]
  92. prev = idxs[0]
  93. ret = [prev]
  94. for i in idxs[1:]:
  95. if i == prev + 1:
  96. if i == idxs[-1]:
  97. ret.extend(['-', i])
  98. else:
  99. if prev != ret[-1]:
  100. ret.extend(['-', prev])
  101. ret.extend([',', i])
  102. prev = i
  103. s = ''.join(map(str, ret))
  104. if fmt_str:
  105. ret = fmt_str.format(s)
  106. else:
  107. proto = addrlist.proto
  108. coin = 'BTC' if proto.coin == 'BCH' and not addrlist.cfg.cashaddr else proto.coin
  109. mmtype = addrlist.al_id.mmtype
  110. ret = '{}{}{}[{}]'.format(
  111. addrlist.al_id.sid,
  112. (f'-{coin}', '')[coin == 'BTC'],
  113. (f'-{mmtype}', '')[mmtype in ('L', 'E')],
  114. s)
  115. addrlist.dmsg_sc('id_str', ret[8:].split('[')[0])
  116. return str.__new__(cls, ret)
  117. class AddrListData(list, MMGenObject):
  118. pass
  119. class AddrList(MMGenObject): # Address info for a single seed ID
  120. entry_type = AddrListEntry
  121. main_attr = 'addr'
  122. desc = 'address'
  123. gen_desc = 'address'
  124. gen_desc_pl = 'es'
  125. gen_addrs = True
  126. gen_passwds = False
  127. gen_keys = False
  128. has_keys = False
  129. chksum_rec_f = lambda foo, e: (str(e.idx), e.addr.views[e.addr.view_pref])
  130. def dmsg_sc(self, desc, data):
  131. Msg(f'sc_debug_{desc}: {data}')
  132. def noop(self, desc, data):
  133. pass
  134. def __init__(
  135. self,
  136. cfg,
  137. proto,
  138. *,
  139. infile = '',
  140. al_id = '',
  141. adata = [],
  142. seed = '',
  143. addr_idxs = '',
  144. src = '',
  145. addrlist = '',
  146. keylist = '',
  147. mmtype = None,
  148. key_address_validity_check = None, # None=prompt user, True=check without prompt, False=skip check
  149. skip_chksum = False,
  150. skip_chksum_msg = False,
  151. add_p2pkh = False):
  152. self.cfg = cfg
  153. self.ka_validity_chk = key_address_validity_check
  154. self.add_p2pkh = add_p2pkh
  155. self.proto = proto
  156. do_chksum = False
  157. if not cfg.debug_addrlist:
  158. self.dmsg_sc = self.noop
  159. if seed and addr_idxs: # data from seed + idxs
  160. self.al_id = AddrListID(sid=seed.sid, mmtype=MMGenAddrType(proto, mmtype or proto.dfl_mmtype))
  161. src = 'gen'
  162. adata = self.generate(
  163. seed,
  164. addr_idxs if isinstance(addr_idxs, AddrIdxList) else AddrIdxList(fmt_str=addr_idxs))
  165. do_chksum = True
  166. elif infile: # data from MMGen address file
  167. self.infile = infile
  168. adata = self.file.parse_file(infile) # sets self.al_id
  169. do_chksum = True
  170. elif al_id and adata: # data from tracking wallet
  171. self.al_id = al_id
  172. elif addrlist: # data from flat address list
  173. self.al_id = None
  174. from .util import remove_dups
  175. addrlist = remove_dups(addrlist, edesc='address', desc='address list')
  176. adata = AddrListData([AddrListEntry(proto=proto, addr=a) for a in addrlist])
  177. elif keylist: # data from flat key list
  178. self.al_id = None
  179. from .util import remove_dups
  180. keylist = remove_dups(keylist, edesc='key', desc='key list', hide=True)
  181. adata = AddrListData([AddrListEntry(proto=proto, sec=PrivKey(proto=proto, wif=k)) for k in keylist])
  182. elif seed or addr_idxs:
  183. die(3, 'Must specify both seed and addr indexes')
  184. elif al_id or adata:
  185. die(3, 'Must specify both al_id and adata')
  186. else:
  187. die(3, f'Incorrect arguments for {type(self).__name__}')
  188. # al_id, adata now set
  189. self.data = adata
  190. self.num_addrs = len(adata)
  191. self.fmt_data = ''
  192. self.chksum = None
  193. if self.al_id is None:
  194. return
  195. if type(self) is ViewKeyAddrList:
  196. if not 'viewkey' in self.al_id.mmtype.extra_attrs:
  197. die(1, f'viewkeys not supported for address type {self.al_id.mmtype.desc!r}')
  198. self.id_str = AddrListIDStr(self)
  199. if type(self) is KeyList:
  200. return
  201. if do_chksum and not skip_chksum:
  202. self.chksum = AddrListChksum(self)
  203. if not skip_chksum_msg:
  204. self.do_chksum_msg(record=src=='gen')
  205. def do_chksum_msg(self, record):
  206. chk = 'Check this value against your records'
  207. rec = f'Record this checksum: it will be used to verify the {self.desc} file in the future'
  208. self.cfg._util.qmsg(
  209. f'Checksum for {self.desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' +
  210. (chk, rec)[record])
  211. def generate(self, seed, addr_idxs):
  212. seed = self.scramble_seed(seed.data)
  213. self.dmsg_sc('seed', seed[:8].hex())
  214. mmtype = self.al_id.mmtype
  215. gen_wallet_passwd = type(self) in (KeyAddrList, ViewKeyAddrList) and 'wallet_passwd' in mmtype.extra_attrs
  216. gen_viewkey = type(self) in (KeyAddrList, ViewKeyAddrList) and 'viewkey' in mmtype.extra_attrs
  217. if self.gen_addrs:
  218. from .keygen import KeyGenerator
  219. from .addrgen import AddrGenerator
  220. kg = KeyGenerator(self.cfg, self.proto, mmtype.pubkey_type)
  221. ag = AddrGenerator(self.cfg, self.proto, mmtype)
  222. if self.add_p2pkh:
  223. ag2 = AddrGenerator(self.cfg, self.proto, 'compressed')
  224. from .derive import derive_coin_privkey_bytes
  225. t_addrs = len(addr_idxs)
  226. le = self.entry_type
  227. out = AddrListData()
  228. CR = '\n' if self.cfg.debug_addrlist else '\r'
  229. for pk_bytes in derive_coin_privkey_bytes(seed, addr_idxs):
  230. if not self.cfg.debug:
  231. self.cfg._util.qmsg_r(
  232. f'{CR}Generating {self.gen_desc} #{pk_bytes.idx} ({pk_bytes.pos} of {t_addrs})')
  233. e = le(proto=self.proto, idx=pk_bytes.idx)
  234. e.sec = PrivKey(
  235. self.proto,
  236. pk_bytes.data,
  237. compressed = mmtype.compressed,
  238. pubkey_type = mmtype.pubkey_type)
  239. if self.gen_addrs:
  240. data = kg.gen_data(e.sec)
  241. e.addr = ag.to_addr(data)
  242. if self.add_p2pkh:
  243. e.addr_p2pkh = ag2.to_addr(data)
  244. if gen_viewkey:
  245. e.viewkey = ag.to_viewkey(data)
  246. if gen_wallet_passwd:
  247. e.wallet_passwd = self.gen_wallet_passwd(
  248. e.viewkey.encode() if type(self) is ViewKeyAddrList else e.sec)
  249. elif self.gen_passwds:
  250. e.passwd = self.gen_passwd(e.sec) # TODO - own type
  251. out.append(e)
  252. self.cfg._util.qmsg('{}{}: {} {}{} generated{}'.format(
  253. CR,
  254. self.al_id.hl(),
  255. t_addrs,
  256. self.gen_desc,
  257. suf(t_addrs, self.gen_desc_pl),
  258. ' ' * 15))
  259. return out
  260. def gen_wallet_passwd(self, privbytes):
  261. from .proto.btc.common import hash256
  262. return WalletPassword(hash256(privbytes)[:16].hex())
  263. def check_format(self, addr):
  264. return True # format is checked when added to list entry object
  265. def scramble_seed(self, seed):
  266. is_btcfork = self.proto.base_coin == 'BTC'
  267. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  268. self.dmsg_sc('str', '(none)')
  269. return seed
  270. if self.proto.base_coin == 'ETH':
  271. scramble_key = self.proto.coin.lower()
  272. else:
  273. scramble_key = (self.proto.coin.lower()+':', '')[is_btcfork] + self.al_id.mmtype.name
  274. from .crypto import Crypto
  275. if self.proto.testnet:
  276. scramble_key += ':' + self.proto.network
  277. self.dmsg_sc('str', scramble_key)
  278. return Crypto(self.cfg).scramble_seed(seed, scramble_key.encode())
  279. def idxs(self):
  280. return [e.idx for e in self.data]
  281. def addrs(self):
  282. return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
  283. def addrpairs(self):
  284. return [(e.idx, e.addr) for e in self.data]
  285. def coinaddrs(self):
  286. return [e.addr for e in self.data]
  287. def comments(self):
  288. return [e.comment for e in self.data]
  289. def entry(self, idx):
  290. for e in self.data:
  291. if idx == e.idx:
  292. return e
  293. def coinaddr(self, idx):
  294. for e in self.data:
  295. if idx == e.idx:
  296. return e.addr
  297. def comment(self, idx):
  298. for e in self.data:
  299. if idx == e.idx:
  300. return e.comment
  301. def set_comment(self, idx, comment):
  302. for e in self.data:
  303. if idx == e.idx:
  304. e.comment = comment
  305. def make_reverse_dict_addrlist(self, coinaddrs):
  306. d = MMGenDict()
  307. b = coinaddrs
  308. for e in self.data:
  309. try:
  310. d[b[b.index(e.addr)]] = (MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.comment)
  311. except ValueError:
  312. pass
  313. return d
  314. def add_wifs(self, key_list):
  315. """
  316. Match WIF keys in a flat list to addresses in self by generating all
  317. possible addresses for each key.
  318. """
  319. def gen_addr(pk, t):
  320. at = self.proto.addr_type(t)
  321. from .keygen import KeyGenerator
  322. from .addrgen import AddrGenerator
  323. kg = KeyGenerator(self.cfg, self.proto, at.pubkey_type)
  324. ag = AddrGenerator(self.cfg, self.proto, at)
  325. return ag.to_addr(kg.gen_data(pk))
  326. compressed_types = set(self.proto.mmtypes) - {'L', 'E'}
  327. uncompressed_types = set(self.proto.mmtypes) & {'L', 'E'}
  328. def gen():
  329. for wif in key_list:
  330. pk = PrivKey(proto=self.proto, wif=wif)
  331. for t in (compressed_types if pk.compressed else uncompressed_types):
  332. yield (gen_addr(pk, t), pk)
  333. addrs4keys = dict(gen())
  334. for d in self.data:
  335. if d.addr in addrs4keys:
  336. d.sec = addrs4keys[d.addr]
  337. def list_missing(self, attr):
  338. return [d.addr for d in self.data if not getattr(d, attr)]
  339. @property
  340. def file(self):
  341. if not hasattr(self, '_file'):
  342. from . import addrfile
  343. self._file = getattr(addrfile, type(self).__name__.replace('List', 'File'))(self)
  344. return self._file
  345. class KeyAddrList(AddrList):
  346. desc = 'key-address'
  347. gen_desc = 'key/address pair'
  348. gen_desc_pl = 's'
  349. gen_keys = True
  350. has_keys = True
  351. chksum_rec_f = lambda foo, e: (str(e.idx), e.addr.views[e.addr.view_pref], e.sec.wif)
  352. class ViewKeyAddrList(KeyAddrList):
  353. desc = 'viewkey-address'
  354. gen_desc = 'viewkey/address pair'
  355. chksum_rec_f = lambda foo, e: (str(e.idx), e.addr)
  356. class KeyList(KeyAddrList):
  357. desc = 'key'
  358. gen_desc = 'key'
  359. gen_addrs = False