addrlist.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 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. addrfile = '',
  139. al_id = '',
  140. adata = [],
  141. seed = '',
  142. addr_idxs = '',
  143. src = '',
  144. addrlist = '',
  145. keylist = '',
  146. mmtype = None,
  147. key_address_validity_check = None, # None=prompt user, True=check without prompt, False=skip check
  148. skip_chksum = False,
  149. skip_chksum_msg = False,
  150. add_p2pkh = False):
  151. self.cfg = cfg
  152. self.ka_validity_chk = key_address_validity_check
  153. self.add_p2pkh = add_p2pkh
  154. self.proto = proto
  155. do_chksum = False
  156. if not cfg.debug_addrlist:
  157. self.dmsg_sc = self.noop
  158. if seed and addr_idxs: # data from seed + idxs
  159. self.al_id = AddrListID(sid=seed.sid, mmtype=MMGenAddrType(proto, mmtype or proto.dfl_mmtype))
  160. src = 'gen'
  161. adata = self.generate(seed, addr_idxs if isinstance(addr_idxs, AddrIdxList) else AddrIdxList(addr_idxs))
  162. do_chksum = True
  163. elif addrfile: # data from MMGen address file
  164. self.infile = addrfile
  165. adata = self.file.parse_file(addrfile) # sets self.al_id
  166. do_chksum = True
  167. elif al_id and adata: # data from tracking wallet
  168. self.al_id = al_id
  169. elif addrlist: # data from flat address list
  170. self.al_id = None
  171. from .util import remove_dups
  172. addrlist = remove_dups(addrlist, edesc='address', desc='address list')
  173. adata = AddrListData([AddrListEntry(proto=proto, addr=a) for a in addrlist])
  174. elif keylist: # data from flat key list
  175. self.al_id = None
  176. from .util import remove_dups
  177. keylist = remove_dups(keylist, edesc='key', desc='key list', hide=True)
  178. adata = AddrListData([AddrListEntry(proto=proto, sec=PrivKey(proto=proto, wif=k)) for k in keylist])
  179. elif seed or addr_idxs:
  180. die(3, 'Must specify both seed and addr indexes')
  181. elif al_id or adata:
  182. die(3, 'Must specify both al_id and adata')
  183. else:
  184. die(3, f'Incorrect arguments for {type(self).__name__}')
  185. # al_id, adata now set
  186. self.data = adata
  187. self.num_addrs = len(adata)
  188. self.fmt_data = ''
  189. self.chksum = None
  190. if self.al_id is None:
  191. return
  192. if type(self) is ViewKeyAddrList:
  193. if not 'viewkey' in self.al_id.mmtype.extra_attrs:
  194. die(1, f'viewkeys not supported for address type {self.al_id.mmtype.desc!r}')
  195. self.id_str = AddrListIDStr(self)
  196. if type(self) is KeyList:
  197. return
  198. if do_chksum and not skip_chksum:
  199. self.chksum = AddrListChksum(self)
  200. if not skip_chksum_msg:
  201. self.do_chksum_msg(record=src=='gen')
  202. def do_chksum_msg(self, record):
  203. chk = 'Check this value against your records'
  204. rec = f'Record this checksum: it will be used to verify the {self.desc} file in the future'
  205. self.cfg._util.qmsg(
  206. f'Checksum for {self.desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' +
  207. (chk, rec)[record])
  208. def generate(self, seed, addr_idxs):
  209. seed = self.scramble_seed(seed.data)
  210. self.dmsg_sc('seed', seed[:8].hex())
  211. mmtype = self.al_id.mmtype
  212. gen_wallet_passwd = type(self) in (KeyAddrList, ViewKeyAddrList) and 'wallet_passwd' in mmtype.extra_attrs
  213. gen_viewkey = type(self) in (KeyAddrList, ViewKeyAddrList) and 'viewkey' in mmtype.extra_attrs
  214. if self.gen_addrs:
  215. from .keygen import KeyGenerator
  216. from .addrgen import AddrGenerator
  217. kg = KeyGenerator(self.cfg, self.proto, mmtype.pubkey_type)
  218. ag = AddrGenerator(self.cfg, self.proto, mmtype)
  219. if self.add_p2pkh:
  220. ag2 = AddrGenerator(self.cfg, self.proto, 'compressed')
  221. from .derive import derive_coin_privkey_bytes
  222. t_addrs = len(addr_idxs)
  223. le = self.entry_type
  224. out = AddrListData()
  225. CR = '\n' if self.cfg.debug_addrlist else '\r'
  226. for pk_bytes in derive_coin_privkey_bytes(seed, addr_idxs):
  227. if not self.cfg.debug:
  228. self.cfg._util.qmsg_r(
  229. f'{CR}Generating {self.gen_desc} #{pk_bytes.idx} ({pk_bytes.pos} of {t_addrs})')
  230. e = le(proto=self.proto, idx=pk_bytes.idx)
  231. e.sec = PrivKey(
  232. self.proto,
  233. pk_bytes.data,
  234. compressed = mmtype.compressed,
  235. pubkey_type = mmtype.pubkey_type)
  236. if self.gen_addrs:
  237. data = kg.gen_data(e.sec)
  238. e.addr = ag.to_addr(data)
  239. if self.add_p2pkh:
  240. e.addr_p2pkh = ag2.to_addr(data)
  241. if gen_viewkey:
  242. e.viewkey = ag.to_viewkey(data)
  243. if gen_wallet_passwd:
  244. e.wallet_passwd = self.gen_wallet_passwd(
  245. e.viewkey.encode() if type(self) is ViewKeyAddrList else e.sec)
  246. elif self.gen_passwds:
  247. e.passwd = self.gen_passwd(e.sec) # TODO - own type
  248. out.append(e)
  249. self.cfg._util.qmsg('{}{}: {} {}{} generated{}'.format(
  250. CR,
  251. self.al_id.hl(),
  252. t_addrs,
  253. self.gen_desc,
  254. suf(t_addrs, self.gen_desc_pl),
  255. ' ' * 15))
  256. return out
  257. def gen_wallet_passwd(self, privbytes):
  258. from .proto.btc.common import hash256
  259. return WalletPassword(hash256(privbytes)[:16].hex())
  260. def check_format(self, addr):
  261. return True # format is checked when added to list entry object
  262. def scramble_seed(self, seed):
  263. is_btcfork = self.proto.base_coin == 'BTC'
  264. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  265. self.dmsg_sc('str', '(none)')
  266. return seed
  267. if self.proto.base_coin == 'ETH':
  268. scramble_key = self.proto.coin.lower()
  269. else:
  270. scramble_key = (self.proto.coin.lower()+':', '')[is_btcfork] + self.al_id.mmtype.name
  271. from .crypto import Crypto
  272. if self.proto.testnet:
  273. scramble_key += ':' + self.proto.network
  274. self.dmsg_sc('str', scramble_key)
  275. return Crypto(self.cfg).scramble_seed(seed, scramble_key.encode())
  276. def idxs(self):
  277. return [e.idx for e in self.data]
  278. def addrs(self):
  279. return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
  280. def addrpairs(self):
  281. return [(e.idx, e.addr) for e in self.data]
  282. def coinaddrs(self):
  283. return [e.addr for e in self.data]
  284. def comments(self):
  285. return [e.comment for e in self.data]
  286. def entry(self, idx):
  287. for e in self.data:
  288. if idx == e.idx:
  289. return e
  290. def coinaddr(self, idx):
  291. for e in self.data:
  292. if idx == e.idx:
  293. return e.addr
  294. def comment(self, idx):
  295. for e in self.data:
  296. if idx == e.idx:
  297. return e.comment
  298. def set_comment(self, idx, comment):
  299. for e in self.data:
  300. if idx == e.idx:
  301. e.comment = comment
  302. def make_reverse_dict_addrlist(self, coinaddrs):
  303. d = MMGenDict()
  304. b = coinaddrs
  305. for e in self.data:
  306. try:
  307. d[b[b.index(e.addr)]] = (MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.comment)
  308. except ValueError:
  309. pass
  310. return d
  311. def add_wifs(self, key_list):
  312. """
  313. Match WIF keys in a flat list to addresses in self by generating all
  314. possible addresses for each key.
  315. """
  316. def gen_addr(pk, t):
  317. at = self.proto.addr_type(t)
  318. from .keygen import KeyGenerator
  319. from .addrgen import AddrGenerator
  320. kg = KeyGenerator(self.cfg, self.proto, at.pubkey_type)
  321. ag = AddrGenerator(self.cfg, self.proto, at)
  322. return ag.to_addr(kg.gen_data(pk))
  323. compressed_types = set(self.proto.mmtypes) - {'L', 'E'}
  324. uncompressed_types = set(self.proto.mmtypes) & {'L', 'E'}
  325. def gen():
  326. for wif in key_list:
  327. pk = PrivKey(proto=self.proto, wif=wif)
  328. for t in (compressed_types if pk.compressed else uncompressed_types):
  329. yield (gen_addr(pk, t), pk)
  330. addrs4keys = dict(gen())
  331. for d in self.data:
  332. if d.addr in addrs4keys:
  333. d.sec = addrs4keys[d.addr]
  334. def list_missing(self, attr):
  335. return [d.addr for d in self.data if not getattr(d, attr)]
  336. @property
  337. def file(self):
  338. if not hasattr(self, '_file'):
  339. from . import addrfile
  340. self._file = getattr(addrfile, type(self).__name__.replace('List', 'File'))(self)
  341. return self._file
  342. class KeyAddrList(AddrList):
  343. desc = 'key-address'
  344. gen_desc = 'key/address pair'
  345. gen_desc_pl = 's'
  346. gen_keys = True
  347. has_keys = True
  348. chksum_rec_f = lambda foo, e: (str(e.idx), e.addr.views[e.addr.view_pref], e.sec.wif)
  349. class ViewKeyAddrList(KeyAddrList):
  350. desc = 'viewkey-address'
  351. gen_desc = 'viewkey/address pair'
  352. chksum_rec_f = lambda foo, e: (str(e.idx), e.addr)
  353. class KeyList(KeyAddrList):
  354. desc = 'key'
  355. gen_desc = 'key'
  356. gen_addrs = False