addr.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2021 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. addr.py: Address generation/display routines for the MMGen suite
  20. """
  21. from hashlib import sha256,sha512
  22. from .common import *
  23. from .obj import *
  24. from .baseconv import *
  25. from .protocol import init_proto,hash160
  26. pnm = g.proj_name
  27. def dmsg_sc(desc,data):
  28. if g.debug_addrlist:
  29. Msg(f'sc_debug_{desc}: {data}')
  30. class AddrGenerator(MMGenObject):
  31. def __new__(cls,proto,addr_type):
  32. if type(addr_type) == str:
  33. addr_type = MMGenAddrType(proto=proto,id_str=addr_type)
  34. elif type(addr_type) == MMGenAddrType:
  35. assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {proto.coin}'
  36. else:
  37. raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
  38. addr_generators = {
  39. 'p2pkh': AddrGeneratorP2PKH,
  40. 'segwit': AddrGeneratorSegwit,
  41. 'bech32': AddrGeneratorBech32,
  42. 'ethereum': AddrGeneratorEthereum,
  43. 'zcash_z': AddrGeneratorZcashZ,
  44. 'monero': AddrGeneratorMonero,
  45. }
  46. me = super(cls,cls).__new__(addr_generators[addr_type.gen_method])
  47. me.desc = type(me).__name__
  48. me.proto = proto
  49. me.addr_type = addr_type
  50. me.pubkey_type = addr_type.pubkey_type
  51. return me
  52. class AddrGeneratorP2PKH(AddrGenerator):
  53. def to_addr(self,pubhex):
  54. assert pubhex.privkey.pubkey_type == self.pubkey_type
  55. return CoinAddr(self.proto,self.proto.pubhash2addr(hash160(pubhex),p2sh=False))
  56. def to_segwit_redeem_script(self,pubhex):
  57. raise NotImplementedError('Segwit redeem script not supported by this address type')
  58. class AddrGeneratorSegwit(AddrGenerator):
  59. def to_addr(self,pubhex):
  60. assert pubhex.privkey.pubkey_type == self.pubkey_type
  61. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  62. return CoinAddr(self.proto,self.proto.pubhex2segwitaddr(pubhex))
  63. def to_segwit_redeem_script(self,pubhex):
  64. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  65. return HexStr(self.proto.pubhex2redeem_script(pubhex))
  66. class AddrGeneratorBech32(AddrGenerator):
  67. def to_addr(self,pubhex):
  68. assert pubhex.privkey.pubkey_type == self.pubkey_type
  69. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  70. return CoinAddr(self.proto,self.proto.pubhash2bech32addr(hash160(pubhex)))
  71. def to_segwit_redeem_script(self,pubhex):
  72. raise NotImplementedError('Segwit redeem script not supported by this address type')
  73. class AddrGeneratorEthereum(AddrGenerator):
  74. def __init__(self,proto,addr_type):
  75. try:
  76. assert not g.use_internal_keccak_module
  77. from sha3 import keccak_256
  78. except:
  79. from .keccak import keccak_256
  80. self.keccak_256 = keccak_256
  81. from .protocol import hash256
  82. self.hash256 = hash256
  83. def to_addr(self,pubhex):
  84. assert pubhex.privkey.pubkey_type == self.pubkey_type
  85. return CoinAddr(self.proto,self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
  86. def to_wallet_passwd(self,sk_hex):
  87. return WalletPassword(self.hash256(sk_hex)[:32])
  88. def to_segwit_redeem_script(self,pubhex):
  89. raise NotImplementedError('Segwit redeem script not supported by this address type')
  90. # github.com/FiloSottile/zcash-mini/zcash/address.go
  91. class AddrGeneratorZcashZ(AddrGenerator):
  92. def zhash256(self,s,t):
  93. s = bytearray(s + bytes(32))
  94. s[0] |= 0xc0
  95. s[32] = t
  96. from .sha2 import Sha256
  97. return Sha256(s,preprocess=False).digest()
  98. def to_addr(self,pubhex): # pubhex is really privhex
  99. assert pubhex.privkey.pubkey_type == self.pubkey_type
  100. key = bytes.fromhex(pubhex)
  101. assert len(key) == 32, f'{len(key)}: incorrect privkey length'
  102. from nacl.bindings import crypto_scalarmult_base
  103. p2 = crypto_scalarmult_base(self.zhash256(key,1))
  104. from .protocol import _b58chk_encode
  105. ver_bytes = self.proto.addr_fmt_to_ver_bytes('zcash_z')
  106. ret = _b58chk_encode(ver_bytes + self.zhash256(key,0) + p2)
  107. return CoinAddr(self.proto,ret)
  108. def to_viewkey(self,pubhex): # pubhex is really privhex
  109. key = bytes.fromhex(pubhex)
  110. assert len(key) == 32, f'{len(key)}: incorrect privkey length'
  111. vk = bytearray(self.zhash256(key,0)+self.zhash256(key,1))
  112. vk[32] &= 0xf8
  113. vk[63] &= 0x7f
  114. vk[63] |= 0x40
  115. from .protocol import _b58chk_encode
  116. ver_bytes = self.proto.addr_fmt_to_ver_bytes('viewkey')
  117. ret = _b58chk_encode(ver_bytes + vk)
  118. return ZcashViewKey(self.proto,ret)
  119. def to_segwit_redeem_script(self,pubhex):
  120. raise NotImplementedError('Zcash z-addresses incompatible with Segwit')
  121. class AddrGeneratorMonero(AddrGenerator):
  122. def __init__(self,proto,addr_type):
  123. try:
  124. assert not g.use_internal_keccak_module
  125. from sha3 import keccak_256
  126. except:
  127. from .keccak import keccak_256
  128. self.keccak_256 = keccak_256
  129. from .protocol import hash256
  130. self.hash256 = hash256
  131. if getattr(opt,'use_old_ed25519',False):
  132. from .ed25519 import edwards,encodepoint,B,scalarmult
  133. else:
  134. from .ed25519ll_djbec import scalarmult
  135. from .ed25519 import edwards,encodepoint,B
  136. self.edwards = edwards
  137. self.encodepoint = encodepoint
  138. self.scalarmult = scalarmult
  139. self.B = B
  140. def b58enc(self,addr_bytes):
  141. enc = baseconv.frombytes
  142. l = len(addr_bytes)
  143. a = ''.join([enc(addr_bytes[i*8:i*8+8],'b58',pad=11,tostr=True) for i in range(l//8)])
  144. b = enc(addr_bytes[l-l%8:],'b58',pad=7,tostr=True)
  145. return a + b
  146. def to_addr(self,sk_hex): # sk_hex instead of pubhex
  147. assert sk_hex.privkey.pubkey_type == self.pubkey_type
  148. # Source and license for scalarmultbase function:
  149. # https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
  150. # Copyright (c) 2014-2016, The Monero Project
  151. # All rights reserved.
  152. def scalarmultbase(e):
  153. if e == 0: return [0, 1]
  154. Q = self.scalarmult(self.B, e//2)
  155. Q = self.edwards(Q, Q)
  156. if e & 1: Q = self.edwards(Q, self.B)
  157. return Q
  158. def hex2int_le(hexstr):
  159. return int((bytes.fromhex(hexstr)[::-1]).hex(),16)
  160. vk_hex = self.to_viewkey(sk_hex)
  161. pk_str = self.encodepoint(scalarmultbase(hex2int_le(sk_hex)))
  162. pvk_str = self.encodepoint(scalarmultbase(hex2int_le(vk_hex)))
  163. addr_p1 = self.proto.addr_fmt_to_ver_bytes('monero') + pk_str + pvk_str
  164. return CoinAddr(
  165. proto = self.proto,
  166. addr = self.b58enc(addr_p1 + self.keccak_256(addr_p1).digest()[:4]) )
  167. def to_wallet_passwd(self,sk_hex):
  168. return WalletPassword(self.hash256(sk_hex)[:32])
  169. def to_viewkey(self,sk_hex):
  170. assert len(sk_hex) == 64, f'{len(sk_hex)}: incorrect privkey length'
  171. return MoneroViewKey(
  172. self.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).digest(),None).hex() )
  173. def to_segwit_redeem_script(self,sk_hex):
  174. raise NotImplementedError('Monero addresses incompatible with Segwit')
  175. class KeyGenerator(MMGenObject):
  176. def __new__(cls,proto,addr_type,generator=None,silent=False):
  177. if type(addr_type) == str: # allow override w/o check
  178. pubkey_type = addr_type
  179. elif type(addr_type) == MMGenAddrType:
  180. assert addr_type in proto.mmtypes, f'{address}: invalid address type for coin {proto.coin}'
  181. pubkey_type = addr_type.pubkey_type
  182. else:
  183. raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
  184. if pubkey_type == 'std':
  185. if cls.test_for_secp256k1(silent=silent) and generator != 1:
  186. if not opt.key_generator or opt.key_generator == 2 or generator == 2:
  187. me = super(cls,cls).__new__(KeyGeneratorSecp256k1)
  188. else:
  189. qmsg('Using (slow) native Python ECDSA library for address generation')
  190. me = super(cls,cls).__new__(KeyGeneratorPython)
  191. elif pubkey_type in ('zcash_z','monero'):
  192. me = super(cls,cls).__new__(KeyGeneratorDummy)
  193. me.desc = 'mmgen-'+pubkey_type
  194. else:
  195. raise ValueError(f'{pubkey_type}: invalid pubkey_type argument')
  196. me.proto = proto
  197. return me
  198. @classmethod
  199. def test_for_secp256k1(self,silent=False):
  200. try:
  201. from .secp256k1 import priv2pub
  202. m = 'Unable to execute priv2pub() from secp256k1 extension module'
  203. assert priv2pub(bytes.fromhex('deadbeef'*8),1),m
  204. return True
  205. except Exception as e:
  206. if not silent:
  207. ymsg(str(e))
  208. return False
  209. class KeyGeneratorPython(KeyGenerator):
  210. desc = 'mmgen-python-ecdsa'
  211. # devdoc/guide_wallets.md:
  212. # Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or
  213. # 0x02 depending on whether they're greater or less than the midpoint of the curve.
  214. def privnum2pubhex(self,numpriv,compressed=False):
  215. import ecdsa
  216. pko = ecdsa.SigningKey.from_secret_exponent(numpriv,curve=ecdsa.SECP256k1)
  217. # pubkey = x (32 bytes) + y (32 bytes) (unsigned big-endian)
  218. pubkey = pko.get_verifying_key().to_string().hex()
  219. if compressed: # discard Y coord, replace with appropriate version byte
  220. # even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
  221. return ('03','02')[pubkey[-1] in '02468ace'] + pubkey[:64]
  222. else:
  223. return '04' + pubkey
  224. def to_pubhex(self,privhex):
  225. assert type(privhex) == PrivKey
  226. return PubKey(
  227. s = self.privnum2pubhex(int(privhex,16),compressed=privhex.compressed),
  228. privkey = privhex )
  229. class KeyGeneratorSecp256k1(KeyGenerator):
  230. desc = 'mmgen-secp256k1'
  231. def to_pubhex(self,privhex):
  232. assert type(privhex) == PrivKey
  233. from .secp256k1 import priv2pub
  234. return PubKey(
  235. s = priv2pub(bytes.fromhex(privhex),int(privhex.compressed)).hex(),
  236. privkey = privhex )
  237. class KeyGeneratorDummy(KeyGenerator):
  238. desc = 'mmgen-dummy'
  239. def to_pubhex(self,privhex):
  240. assert type(privhex) == PrivKey
  241. return PubKey(
  242. s = privhex,
  243. privkey = privhex )
  244. class AddrListEntryBase(MMGenListItem):
  245. invalid_attrs = {'proto'}
  246. def __init__(self,proto,**kwargs):
  247. self.__dict__['proto'] = proto
  248. MMGenListItem.__init__(self,**kwargs)
  249. class AddrListEntry(AddrListEntryBase):
  250. addr = ListItemAttr('CoinAddr',include_proto=True)
  251. idx = ListItemAttr('AddrIdx') # not present in flat addrlists
  252. label = ListItemAttr('TwComment',reassign_ok=True)
  253. sec = ListItemAttr('PrivKey',include_proto=True)
  254. viewkey = ListItemAttr('ViewKey',include_proto=True)
  255. wallet_passwd = ListItemAttr('WalletPassword')
  256. class PasswordListEntry(AddrListEntryBase):
  257. passwd = ListItemAttr(str,typeconv=False) # TODO: create Password type
  258. idx = ImmutableAttr('AddrIdx')
  259. label = ListItemAttr('TwComment',reassign_ok=True)
  260. sec = ListItemAttr('PrivKey',include_proto=True)
  261. class AddrListChksum(str,Hilite):
  262. color = 'pink'
  263. trunc_ok = False
  264. def __new__(cls,addrlist):
  265. ea = addrlist.al_id.mmtype.extra_attrs # add viewkey and passwd to the mix, if present
  266. if ea == None: ea = ()
  267. lines = [' '.join(
  268. addrlist.chksum_rec_f(e) +
  269. tuple(getattr(e,a) for a in ea if getattr(e,a))
  270. ) for e in addrlist.data]
  271. return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
  272. class AddrListIDStr(str,Hilite):
  273. color = 'green'
  274. trunc_ok = False
  275. def __new__(cls,addrlist,fmt_str=None):
  276. idxs = [e.idx for e in addrlist.data]
  277. prev = idxs[0]
  278. ret = prev,
  279. for i in idxs[1:]:
  280. if i == prev + 1:
  281. if i == idxs[-1]: ret += '-', i
  282. else:
  283. if prev != ret[-1]: ret += '-', prev
  284. ret += ',', i
  285. prev = i
  286. s = ''.join(map(str,ret))
  287. if fmt_str:
  288. ret = fmt_str.format(s)
  289. else:
  290. bc = (addrlist.proto.base_coin,addrlist.proto.coin)[addrlist.proto.base_coin=='ETH']
  291. mt = addrlist.al_id.mmtype
  292. ret = '{}{}{}[{}]'.format(
  293. addrlist.al_id.sid,
  294. ('-'+bc,'')[bc == 'BTC'],
  295. ('-'+mt,'')[mt in ('L','E')],
  296. s )
  297. dmsg_sc('id_str',ret[8:].split('[')[0])
  298. return str.__new__(cls,ret)
  299. class AddrList(MMGenObject): # Address info for a single seed ID
  300. msgs = {
  301. 'file_header': """
  302. # {pnm} address file
  303. #
  304. # This file is editable.
  305. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  306. # A text label of {n} screen cells or less may be added to the right of each
  307. # address, and it will be appended to the tracking wallet label upon import.
  308. # The label may contain any printable ASCII symbol.
  309. """.strip().format(n=TwComment.max_screen_width,pnm=pnm),
  310. 'record_chksum': """
  311. Record this checksum: it will be used to verify the address file in the future
  312. """.strip(),
  313. 'check_chksum': 'Check this value against your records',
  314. 'removed_dup_keys': f"""
  315. Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
  316. """.strip(),
  317. }
  318. entry_type = AddrListEntry
  319. main_attr = 'addr'
  320. data_desc = 'address'
  321. file_desc = 'addresses'
  322. gen_desc = 'address'
  323. gen_desc_pl = 'es'
  324. gen_addrs = True
  325. gen_passwds = False
  326. gen_keys = False
  327. has_keys = False
  328. ext = 'addrs'
  329. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
  330. line_ctr = 0
  331. def __init__(self,proto,
  332. addrfile = '',
  333. al_id = '',
  334. adata = [],
  335. seed = '',
  336. addr_idxs = '',
  337. src = '',
  338. addrlist = '',
  339. keylist = '',
  340. mmtype = None,
  341. skip_key_address_validity_check = False,
  342. ):
  343. self.skip_ka_check = skip_key_address_validity_check
  344. do_chksum = True
  345. self.update_msgs()
  346. mmtype = mmtype or proto.dfl_mmtype
  347. assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
  348. from .protocol import CoinProtocol
  349. self.bitcoin_addrtypes = tuple(
  350. MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes)
  351. self.proto = proto
  352. if seed and addr_idxs: # data from seed + idxs
  353. self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
  354. adata = self.generate(seed,addr_idxs)
  355. elif addrfile: # data from MMGen address file
  356. self.infile = addrfile
  357. adata = self.parse_file(addrfile) # sets self.al_id
  358. elif al_id and adata: # data from tracking wallet
  359. self.al_id = al_id
  360. do_chksum = False
  361. elif addrlist: # data from flat address list
  362. self.al_id = None
  363. addrlist = remove_dups(addrlist,edesc='address',desc='address list')
  364. adata = AddrListData([AddrListEntry(proto=proto,addr=a) for a in addrlist])
  365. elif keylist: # data from flat key list
  366. self.al_id = None
  367. keylist = remove_dups(keylist,edesc='key',desc='key list',hide=True)
  368. adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in keylist])
  369. elif seed or addr_idxs:
  370. die(3,'Must specify both seed and addr indexes')
  371. elif al_id or adata:
  372. die(3,'Must specify both al_id and adata')
  373. else:
  374. die(3,f'Incorrect arguments for {type(self).__name__}')
  375. # al_id,adata now set
  376. self.data = adata
  377. self.num_addrs = len(adata)
  378. self.fmt_data = ''
  379. self.chksum = None
  380. if self.al_id == None: return
  381. self.id_str = AddrListIDStr(self)
  382. if type(self) == KeyList: return
  383. if do_chksum:
  384. self.chksum = AddrListChksum(self)
  385. qmsg(f'Checksum for {self.data_desc} data {self.id_str.hl()}: {self.chksum.hl()}')
  386. qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']])
  387. def update_msgs(self):
  388. self.msgs = AddrList.msgs
  389. self.msgs.update(type(self).msgs)
  390. def generate(self,seed,addrnums):
  391. assert type(addrnums) is AddrIdxList
  392. seed = self.scramble_seed(seed.data)
  393. dmsg_sc('seed',seed[:8].hex())
  394. compressed = self.al_id.mmtype.compressed
  395. pubkey_type = self.al_id.mmtype.pubkey_type
  396. gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in self.al_id.mmtype.extra_attrs
  397. gen_viewkey = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs
  398. if self.gen_addrs:
  399. kg = KeyGenerator(self.proto,self.al_id.mmtype)
  400. ag = AddrGenerator(self.proto,self.al_id.mmtype)
  401. t_addrs,num,pos,out = len(addrnums),0,0,AddrListData()
  402. le = self.entry_type
  403. while pos != t_addrs:
  404. seed = sha512(seed).digest()
  405. num += 1 # round
  406. if num != addrnums[pos]: continue
  407. pos += 1
  408. if not g.debug:
  409. qmsg_r(f'\rGenerating {self.gen_desc} #{num} ({pos} of {t_addrs})')
  410. e = le(proto=self.proto,idx=num)
  411. # Secret key is double sha256 of seed hash round /num/
  412. e.sec = PrivKey(
  413. self.proto,
  414. sha256(sha256(seed).digest()).digest(),
  415. compressed = compressed,
  416. pubkey_type = pubkey_type )
  417. if self.gen_addrs:
  418. pubhex = kg.to_pubhex(e.sec)
  419. e.addr = ag.to_addr(pubhex)
  420. if gen_viewkey:
  421. e.viewkey = ag.to_viewkey(pubhex)
  422. if gen_wallet_passwd:
  423. e.wallet_passwd = ag.to_wallet_passwd(e.sec)
  424. if type(self) == PasswordList:
  425. e.passwd = str(self.make_passwd(e.sec)) # TODO - own type
  426. dmsg(f'Key {pos:>03}: {e.passwd}')
  427. out.append(e)
  428. if g.debug_addrlist:
  429. Msg(f'generate():\n{e.pfmt()}')
  430. qmsg('\r{}: {} {}{} generated{}'.format(
  431. self.al_id.hl(),
  432. t_addrs,
  433. self.gen_desc,
  434. suf(t_addrs,self.gen_desc_pl),
  435. ' ' * 15 ))
  436. return out
  437. def check_format(self,addr):
  438. return True # format is checked when added to list entry object
  439. def scramble_seed(self,seed):
  440. is_btcfork = self.proto.base_coin == 'BTC'
  441. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  442. dmsg_sc('str','(none)')
  443. return seed
  444. if self.proto.base_coin == 'ETH':
  445. scramble_key = self.proto.coin.lower()
  446. else:
  447. scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  448. from .crypto import scramble_seed
  449. if self.proto.testnet:
  450. scramble_key += ':' + self.proto.network
  451. dmsg_sc('str',scramble_key)
  452. return scramble_seed(seed,scramble_key.encode())
  453. def encrypt(self,desc='new key list'):
  454. from .crypto import mmgen_encrypt
  455. self.fmt_data = mmgen_encrypt(self.fmt_data.encode(),desc,'')
  456. self.ext += '.'+g.mmenc_ext
  457. def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
  458. tn = ('.' + self.proto.network) if self.proto.testnet else ''
  459. fn = '{}{x}{}.{}'.format(self.id_str,tn,self.ext,x='-α' if g.debug_utf8 else '')
  460. ask_tty = self.has_keys and not opt.quiet
  461. write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary)
  462. def idxs(self):
  463. return [e.idx for e in self.data]
  464. def addrs(self):
  465. return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
  466. def addrpairs(self):
  467. return [(e.idx,e.addr) for e in self.data]
  468. def coinaddrs(self):
  469. return [e.addr for e in self.data]
  470. def comments(self):
  471. return [e.label for e in self.data]
  472. def entry(self,idx):
  473. for e in self.data:
  474. if idx == e.idx: return e
  475. def coinaddr(self,idx):
  476. for e in self.data:
  477. if idx == e.idx: return e.addr
  478. def comment(self,idx):
  479. for e in self.data:
  480. if idx == e.idx: return e.label
  481. def set_comment(self,idx,comment):
  482. for e in self.data:
  483. if idx == e.idx:
  484. e.label = comment
  485. def make_reverse_dict_addrlist(self,coinaddrs):
  486. d = MMGenDict()
  487. b = coinaddrs
  488. for e in self.data:
  489. try:
  490. d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.label )
  491. except ValueError:
  492. pass
  493. return d
  494. def remove_dup_keys(self,cmplist):
  495. assert self.has_keys
  496. pop_list = []
  497. for n,d in enumerate(self.data):
  498. for e in cmplist.data:
  499. if e.sec.wif == d.sec.wif:
  500. pop_list.append(n)
  501. for n in reversed(pop_list): self.data.pop(n)
  502. if pop_list:
  503. vmsg(self.msgs['removed_dup_keys'].format(len(pop_list),suf(removed)))
  504. def add_wifs(self,key_list):
  505. if not key_list: return
  506. for d in self.data:
  507. for e in key_list.data:
  508. if e.addr and e.sec and e.addr == d.addr:
  509. d.sec = e.sec
  510. def list_missing(self,key):
  511. return [d.addr for d in self.data if not getattr(d,key)]
  512. def generate_addrs_from_keys(self):
  513. # assume that the first listed mmtype is valid for flat key list
  514. at = self.proto.addr_type(self.proto.mmtypes[0])
  515. kg = KeyGenerator(self.proto,at.pubkey_type)
  516. ag = AddrGenerator(self.proto,at)
  517. d = self.data
  518. for n,e in enumerate(d,1):
  519. qmsg_r(f'\rGenerating addresses from keylist: {n}/{len(d)}')
  520. e.addr = ag.to_addr(kg.to_pubhex(e.sec))
  521. if g.debug_addrlist:
  522. Msg(f'generate_addrs_from_keys():\n{e.pfmt()}')
  523. qmsg(f'\rGenerated addresses from keylist: {n}/{len(d)} ')
  524. def make_label(self):
  525. bc,mt = self.proto.base_coin,self.al_id.mmtype
  526. l_coin = [] if bc == 'BTC' else [self.proto.coin] if bc == 'ETH' else [bc]
  527. l_type = [] if mt == 'E' or (mt == 'L' and not self.proto.testnet) else [mt.name.upper()]
  528. l_tn = [] if not self.proto.testnet else [self.proto.network.upper()]
  529. lbl_p2 = ':'.join(l_coin+l_type+l_tn)
  530. return self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2
  531. def format(self,add_comments=False):
  532. out = [self.msgs['file_header']+'\n']
  533. if self.chksum:
  534. out.append(f'# {capfirst(self.data_desc)} data checksum for {self.id_str}: {self.chksum}')
  535. out.append('# Record this value to a secure location.\n')
  536. lbl = self.make_label()
  537. dmsg_sc('lbl',lbl[9:])
  538. out.append(f'{lbl} {{')
  539. fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx))
  540. for e in self.data:
  541. c = ' '+e.label if add_comments and e.label else ''
  542. if type(self) == KeyList:
  543. out.append(fs.format( e.idx, f'{self.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
  544. elif type(self) == PasswordList:
  545. out.append(fs.format(e.idx,e.passwd,c))
  546. else: # First line with idx
  547. out.append(fs.format(e.idx,e.addr,c))
  548. if self.has_keys:
  549. if opt.b16:
  550. out.append(fs.format( '', f'orig_hex: {e.sec.orig_hex}', c ))
  551. out.append(fs.format( '', f'{self.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
  552. for k in ('viewkey','wallet_passwd'):
  553. v = getattr(e,k)
  554. if v: out.append(fs.format( '', f'{k}: {v}', c ))
  555. out.append('}')
  556. self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
  557. def get_line(self,lines):
  558. ret = lines.pop(0).split(None,2)
  559. self.line_ctr += 1
  560. if ret[0] == 'orig_hex:': # hacky
  561. ret = lines.pop(0).split(None,2)
  562. self.line_ctr += 1
  563. return ret if len(ret) == 3 else ret + ['']
  564. def parse_file_body(self,lines):
  565. ret = AddrListData()
  566. le = self.entry_type
  567. iifs = "{!r}: invalid identifier [expected '{}:']"
  568. while lines:
  569. idx,addr,lbl = self.get_line(lines)
  570. assert is_mmgen_idx(idx), f'invalid address index {idx!r}'
  571. self.check_format(addr)
  572. a = le(**{ 'proto': self.proto, 'idx':int(idx), self.main_attr:addr, 'label':lbl })
  573. if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
  574. d = self.get_line(lines)
  575. assert d[0] == self.al_id.mmtype.wif_label+':', iifs.format(d[0],self.al_id.mmtype.wif_label)
  576. a.sec = PrivKey(proto=self.proto,wif=d[1])
  577. for k,dtype,add_proto in (
  578. ('viewkey',ViewKey,True),
  579. ('wallet_passwd',WalletPassword,False) ):
  580. if k in self.al_id.mmtype.extra_attrs:
  581. d = self.get_line(lines)
  582. assert d[0] == k+':', iifs.format(d[0],k)
  583. setattr(a,k,dtype( *((self.proto,d[1]) if add_proto else (d[1],)) ) )
  584. ret.append(a)
  585. if self.has_keys and not self.skip_ka_check:
  586. if getattr(opt,'yes',False) or keypress_confirm('Check key-to-address validity?'):
  587. kg = KeyGenerator(self.proto,self.al_id.mmtype)
  588. ag = AddrGenerator(self.proto,self.al_id.mmtype)
  589. llen = len(ret)
  590. for n,e in enumerate(ret):
  591. qmsg_r(f'\rVerifying keys {n+1}/{llen}')
  592. assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),(
  593. f'Key doesn’t match address!\n {e.sec.wif}\n {e.addr}')
  594. qmsg(' - done')
  595. return ret
  596. def parse_file(self,fn,buf=[],exit_on_error=True):
  597. def parse_addrfile_label(lbl):
  598. """
  599. label examples:
  600. - Bitcoin legacy mainnet: no label
  601. - Bitcoin legacy testnet: 'LEGACY:TESTNET'
  602. - Bitcoin Segwit: 'SEGWIT'
  603. - Bitcoin Segwit testnet: 'SEGWIT:TESTNET'
  604. - Bitcoin Bech32 regtest: 'BECH32:REGTEST'
  605. - Litecoin legacy mainnet: 'LTC'
  606. - Litecoin Bech32 mainnet: 'LTC:BECH32'
  607. - Litecoin legacy testnet: 'LTC:LEGACY:TESTNET'
  608. - Ethereum mainnet: 'ETH'
  609. - Ethereum Classic mainnet: 'ETC'
  610. - Ethereum regtest: 'ETH:REGTEST'
  611. """
  612. lbl = lbl.lower()
  613. # remove the network component:
  614. if lbl.endswith(':testnet'):
  615. network = 'testnet'
  616. lbl = lbl[:-8]
  617. elif lbl.endswith(':regtest'):
  618. network = 'regtest'
  619. lbl = lbl[:-8]
  620. else:
  621. network = 'mainnet'
  622. if lbl in self.bitcoin_addrtypes:
  623. coin,mmtype_key = ( 'BTC', lbl )
  624. elif ':' in lbl: # first component is coin, second is mmtype_key
  625. coin,mmtype_key = lbl.split(':')
  626. else: # only component is coin
  627. coin,mmtype_key = ( lbl, None )
  628. proto = init_proto(coin=coin,network=network)
  629. if mmtype_key == None:
  630. mmtype_key = proto.mmtypes[0]
  631. return ( proto, proto.addr_type(mmtype_key) )
  632. lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
  633. try:
  634. assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})'
  635. ls = lines[0].split()
  636. assert 1 < len(ls) < 5, f'Invalid first line for {self.gen_desc} file: {lines[0]!r}'
  637. assert ls.pop() == '{', f'{ls!r}: invalid first line'
  638. assert lines[-1] == '}', f'{lines[-1]!r}: invalid last line'
  639. sid = ls.pop(0)
  640. assert is_mmgen_seed_id(sid), f'{sid!r}: invalid Seed ID'
  641. if type(self) == PasswordList and len(ls) == 2:
  642. ss = ls.pop().split(':')
  643. assert len(ss) == 2, f'{ss!r}: invalid password length specifier (must contain colon)'
  644. self.set_pw_fmt(ss[0])
  645. self.set_pw_len(ss[1])
  646. self.pw_id_str = MMGenPWIDString(ls.pop())
  647. proto = init_proto('btc')# FIXME: dummy protocol
  648. mmtype = MMGenPasswordType(proto,'P')
  649. elif len(ls) == 1:
  650. proto,mmtype = parse_addrfile_label(ls[0])
  651. elif len(ls) == 0:
  652. proto = init_proto('btc')
  653. mmtype = proto.addr_type('L')
  654. else:
  655. raise ValueError(f'{lines[0]}: Invalid first line for {self.gen_desc} file {fn!r}')
  656. if type(self) != PasswordList:
  657. if proto.base_coin != self.proto.base_coin or proto.network != self.proto.network:
  658. """
  659. Having caller supply protocol and checking address file protocol against it here
  660. allows us to catch all mismatches in one place. This behavior differs from that of
  661. transaction files, which determine the protocol independently, requiring the caller
  662. to check for protocol mismatches (e.g. MMGenTX.check_correct_chain())
  663. """
  664. raise ValueError(
  665. f'{self.data_desc} file is '
  666. + f'{proto.base_coin} {proto.network} but protocol is '
  667. + f'{self.proto.base_coin} {self.proto.network}' )
  668. self.base_coin = proto.base_coin
  669. self.network = proto.network
  670. self.al_id = AddrListID(SeedID(sid=sid),mmtype)
  671. data = self.parse_file_body(lines[1:-1])
  672. assert isinstance(data,list),'Invalid file body data'
  673. except Exception as e:
  674. m = 'Invalid data in {} list file {!r}{} ({!s})'.format(
  675. self.data_desc,
  676. self.infile,
  677. (f', content line {self.line_ctr}' if self.line_ctr else ''),
  678. e )
  679. if exit_on_error:
  680. die(3,m)
  681. else:
  682. msg(m)
  683. return False
  684. return data
  685. class KeyAddrList(AddrList):
  686. data_desc = 'key-address'
  687. file_desc = 'secret keys'
  688. gen_desc = 'key/address pair'
  689. gen_desc_pl = 's'
  690. gen_addrs = True
  691. gen_keys = True
  692. has_keys = True
  693. ext = 'akeys'
  694. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  695. class KeyList(AddrList):
  696. msgs = {
  697. 'file_header': f"""
  698. # {pnm} key file
  699. #
  700. # This file is editable.
  701. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  702. """.strip()
  703. }
  704. data_desc = 'key'
  705. file_desc = 'secret keys'
  706. gen_desc = 'key'
  707. gen_desc_pl = 's'
  708. gen_addrs = False
  709. gen_keys = True
  710. has_keys = True
  711. ext = 'keys'
  712. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  713. def is_bip39_str(s):
  714. from .bip39 import bip39
  715. return bool(bip39.tohex(s.split(),wl_id='bip39'))
  716. def is_xmrseed(s):
  717. return bool(baseconv.tobytes(s.split(),wl_id='xmrseed'))
  718. from collections import namedtuple
  719. class PasswordList(AddrList):
  720. msgs = {
  721. 'file_header': f"""
  722. # {pnm} password file
  723. #
  724. # This file is editable.
  725. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  726. # A text label of {TwComment.max_screen_width} screen cells or less may be added to the right of each
  727. # password. The label may contain any printable ASCII symbol.
  728. #
  729. """.strip(),
  730. 'file_header_mn': f"""
  731. # {pnm} {{}} password file
  732. #
  733. # This file is editable.
  734. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  735. #
  736. """.strip(),
  737. 'record_chksum': """
  738. Record this checksum: it will be used to verify the password file in the future
  739. """.strip()
  740. }
  741. entry_type = PasswordListEntry
  742. main_attr = 'passwd'
  743. data_desc = 'password'
  744. file_desc = 'passwords'
  745. gen_desc = 'password'
  746. gen_desc_pl = 's'
  747. gen_addrs = False
  748. gen_keys = False
  749. gen_passwds = True
  750. has_keys = False
  751. ext = 'pws'
  752. pw_len = None
  753. dfl_pw_fmt = 'b58'
  754. pwinfo = namedtuple('passwd_info',['min_len','max_len','dfl_len','valid_lens','desc','chk_func'])
  755. pw_info = {
  756. 'b32': pwinfo(10, 42 ,24, None, 'base32 password', is_b32_str), # 32**24 < 2**128
  757. 'b58': pwinfo(8, 36 ,20, None, 'base58 password', is_b58_str), # 58**20 < 2**128
  758. 'bip39': pwinfo(12, 24 ,24, [12,18,24],'BIP39 mnemonic', is_bip39_str),
  759. 'xmrseed': pwinfo(25, 25, 25, [25], 'Monero new-style mnemonic',is_xmrseed),
  760. 'hex': pwinfo(32, 64 ,64, [32,48,64],'hexadecimal password', is_hex_str),
  761. }
  762. chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
  763. feature_warn_fs = 'WARNING: {!r} is a potentially dangerous feature. Use at your own risk!'
  764. hex2bip39 = False
  765. def __init__(self,proto,
  766. infile = None,
  767. seed = None,
  768. pw_idxs = None,
  769. pw_id_str = None,
  770. pw_len = None,
  771. pw_fmt = None,
  772. chk_params_only = False
  773. ):
  774. self.proto = proto # proto is ignored
  775. self.update_msgs()
  776. if infile:
  777. self.infile = infile
  778. self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
  779. else:
  780. if not chk_params_only:
  781. for k in (seed,pw_idxs):
  782. assert k
  783. self.pw_id_str = MMGenPWIDString(pw_id_str)
  784. self.set_pw_fmt(pw_fmt)
  785. self.set_pw_len(pw_len)
  786. if chk_params_only:
  787. return
  788. if self.hex2bip39:
  789. ymsg(self.feature_warn_fs.format(pw_fmt))
  790. self.set_pw_len_vs_seed_len(pw_len,seed)
  791. self.al_id = AddrListID(seed.sid,MMGenPasswordType(self.proto,'P'))
  792. self.data = self.generate(seed,pw_idxs)
  793. if self.pw_fmt in ('bip39','xmrseed'):
  794. self.msgs['file_header'] = self.msgs['file_header_mn'].format(self.pw_fmt.upper())
  795. self.num_addrs = len(self.data)
  796. self.fmt_data = ''
  797. self.chksum = AddrListChksum(self)
  798. fs = f'{self.al_id.sid}-{self.pw_id_str}-{self.pw_fmt_disp}-{self.pw_len}[{{}}]'
  799. self.id_str = AddrListIDStr(self,fs)
  800. qmsg(f'Checksum for {self.data_desc} data {self.id_str.hl()}: {self.chksum.hl()}')
  801. qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]])
  802. def set_pw_fmt(self,pw_fmt):
  803. if pw_fmt == 'hex2bip39':
  804. self.hex2bip39 = True
  805. self.pw_fmt = 'bip39'
  806. self.pw_fmt_disp = 'hex2bip39'
  807. else:
  808. self.pw_fmt = pw_fmt
  809. self.pw_fmt_disp = pw_fmt
  810. if self.pw_fmt not in self.pw_info:
  811. raise InvalidPasswdFormat(
  812. '{!r}: invalid password format. Valid formats: {}'.format(
  813. self.pw_fmt,
  814. ', '.join(self.pw_info) ))
  815. def chk_pw_len(self,passwd=None):
  816. if passwd is None:
  817. assert self.pw_len,'either passwd or pw_len must be set'
  818. pw_len = self.pw_len
  819. fs = '{l}: invalid user-requested length for {b} ({c}{m})'
  820. else:
  821. pw_len = len(passwd)
  822. fs = '{pw}: {b} has invalid length {l} ({c}{m} characters)'
  823. d = self.pw_info[self.pw_fmt]
  824. if d.valid_lens:
  825. if pw_len not in d.valid_lens:
  826. die(2, fs.format( l=pw_len, b=d.desc, c='not one of ', m=d.valid_lens, pw=passwd ))
  827. elif pw_len > d.max_len:
  828. die(2, fs.format( l=pw_len, b=d.desc, c='>', m=d.max_len, pw=passwd ))
  829. elif pw_len < d.min_len:
  830. die(2, fs.format( l=pw_len, b=d.desc, c='<', m=d.min_len, pw=passwd ))
  831. def set_pw_len(self,pw_len):
  832. d = self.pw_info[self.pw_fmt]
  833. if pw_len is None:
  834. self.pw_len = d.dfl_len
  835. return
  836. if not is_int(pw_len):
  837. die(2,f'{pw_len!r}: invalid user-requested password length (not an integer)')
  838. self.pw_len = int(pw_len)
  839. self.chk_pw_len()
  840. def set_pw_len_vs_seed_len(self,pw_len,seed):
  841. pf = self.pw_fmt
  842. if pf == 'hex':
  843. pw_bytes = self.pw_len // 2
  844. good_pw_len = seed.byte_len * 2
  845. elif pf == 'bip39':
  846. from .bip39 import bip39
  847. pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True)
  848. good_pw_len = bip39.seedlen2nwords(seed.byte_len,in_bytes=True)
  849. elif pf == 'xmrseed':
  850. pw_bytes = baseconv.seedlen_map_rev['xmrseed'][self.pw_len]
  851. try:
  852. good_pw_len = baseconv.seedlen_map['xmrseed'][seed.byte_len]
  853. except:
  854. die(1,f'{seed.byte_len*8}: unsupported seed length for Monero new-style mnemonic')
  855. elif pf in ('b32','b58'):
  856. pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
  857. pw_bytes = pw_int.bit_length() // 8
  858. good_pw_len = len(baseconv.frombytes(b'\xff'*seed.byte_len,wl_id=pf))
  859. else:
  860. raise NotImplementedError(f'{pf!r}: unknown password format')
  861. if pw_bytes > seed.byte_len:
  862. die(1,
  863. 'Cannot generate passwords with more entropy than underlying seed! ({} bits)\n'.format(
  864. len(seed.data) * 8 ) + (
  865. 'Re-run the command with --passwd-len={}' if pf in ('bip39','hex') else
  866. 'Re-run the command, specifying a password length of {} or less'
  867. ).format(good_pw_len) )
  868. if pf in ('bip39','hex') and pw_bytes < seed.byte_len:
  869. if not keypress_confirm(
  870. f'WARNING: requested {self.pw_info[pf].desc} length has less entropy ' +
  871. 'than underlying seed!\nIs this what you want?',
  872. default_yes = True ):
  873. die(1,'Exiting at user request')
  874. def make_passwd(self,hex_sec):
  875. assert self.pw_fmt in self.pw_info
  876. if self.pw_fmt == 'hex':
  877. # take most significant part
  878. return hex_sec[:self.pw_len]
  879. elif self.pw_fmt == 'bip39':
  880. from .bip39 import bip39
  881. pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True)
  882. # take most significant part
  883. return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39'))
  884. elif self.pw_fmt == 'xmrseed':
  885. pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2
  886. # take most significant part
  887. bytes_trunc = bytes.fromhex(hex_sec[:pw_len_hex])
  888. bytes_preproc = init_proto('xmr').preprocess_key(bytes_trunc,None)
  889. return ' '.join(baseconv.frombytes(bytes_preproc,wl_id='xmrseed'))
  890. else:
  891. # take least significant part
  892. return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
  893. def check_format(self,pw):
  894. if not self.pw_info[self.pw_fmt].chk_func(pw):
  895. raise ValueError(f'Password is not valid {self.pw_info[self.pw_fmt].desc} data')
  896. pwlen = len(pw.split()) if self.pw_fmt in ('bip39','xmrseed') else len(pw)
  897. if pwlen != self.pw_len:
  898. raise ValueError(f'Password has incorrect length ({pwlen} != {self.pw_len})')
  899. return True
  900. def scramble_seed(self,seed):
  901. # Changing either pw_fmt or pw_len will cause a different, unrelated
  902. # set of passwords to be generated: this is what we want.
  903. # NB: In original implementation, pw_id_str was 'baseN', not 'bN'
  904. scramble_key = f'{self.pw_fmt}:{self.pw_len}:{self.pw_id_str}'
  905. if self.hex2bip39:
  906. from .bip39 import bip39
  907. pwlen = bip39.nwords2seedlen(self.pw_len,in_hex=True)
  908. scramble_key = f'hex:{pwlen}:{self.pw_id_str}'
  909. from .crypto import scramble_seed
  910. dmsg_sc('str',scramble_key)
  911. return scramble_seed(seed,scramble_key.encode())
  912. def get_line(self,lines):
  913. self.line_ctr += 1
  914. if self.pw_fmt in ('bip39','xmrseed'):
  915. ret = lines.pop(0).split(None,self.pw_len+1)
  916. if len(ret) > self.pw_len+1:
  917. m1 = f'extraneous text {ret[self.pw_len+1]!r} found after password'
  918. m2 = '[bare comments not allowed in BIP39 password files]'
  919. m = m1+' '+m2
  920. elif len(ret) < self.pw_len+1:
  921. m = f'invalid password length {len(ret)-1}'
  922. else:
  923. return (ret[0],' '.join(ret[1:self.pw_len+1]),'')
  924. raise ValueError(m)
  925. else:
  926. ret = lines.pop(0).split(None,2)
  927. return ret if len(ret) == 3 else ret + ['']
  928. def make_label(self):
  929. return f'{self.al_id.sid} {self.pw_id_str} {self.pw_fmt_disp}:{self.pw_len}'
  930. class AddrData(MMGenObject):
  931. msgs = {
  932. 'too_many_acct_addresses': f"""
  933. ERROR: More than one address found for account: '{{}}'.
  934. Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
  935. Please restore your tracking wallet from a backup or create a new one and
  936. re-import your addresses.
  937. """.strip()
  938. }
  939. def __new__(cls,proto,*args,**kwargs):
  940. return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
  941. def __init__(self,proto,*args,**kwargs):
  942. self.al_ids = {}
  943. self.proto = proto
  944. self.rpc = None
  945. def seed_ids(self):
  946. return list(self.al_ids.keys())
  947. def addrlist(self,al_id):
  948. # TODO: Validate al_id
  949. if al_id in self.al_ids:
  950. return self.al_ids[al_id]
  951. def mmaddr2coinaddr(self,mmaddr):
  952. al_id,idx = MMGenID(self.proto,mmaddr).rsplit(':',1)
  953. coinaddr = ''
  954. if al_id in self.al_ids:
  955. coinaddr = self.addrlist(al_id).coinaddr(int(idx))
  956. return coinaddr or None
  957. def coinaddr2mmaddr(self,coinaddr):
  958. d = self.make_reverse_dict([coinaddr])
  959. return (list(d.values())[0][0]) if d else None
  960. def add(self,addrlist):
  961. if type(addrlist) == AddrList:
  962. self.al_ids[addrlist.al_id] = addrlist
  963. return True
  964. else:
  965. raise TypeError(f'Error: object {addrlist!r} is not of type AddrList')
  966. def make_reverse_dict(self,coinaddrs):
  967. d = MMGenDict()
  968. for al_id in self.al_ids:
  969. d.update(self.al_ids[al_id].make_reverse_dict_addrlist(coinaddrs))
  970. return d
  971. class TwAddrData(AddrData,metaclass=AsyncInit):
  972. def __new__(cls,proto,*args,**kwargs):
  973. return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
  974. async def __init__(self,proto,wallet=None):
  975. self.proto = proto
  976. from .rpc import rpc_init
  977. self.rpc = await rpc_init(proto)
  978. self.al_ids = {}
  979. await self.add_tw_data(wallet)
  980. async def get_tw_data(self,wallet=None):
  981. vmsg('Getting address data from tracking wallet')
  982. c = self.rpc
  983. if 'label_api' in c.caps:
  984. accts = await c.call('listlabels')
  985. ll = await c.batch_call('getaddressesbylabel',[(k,) for k in accts])
  986. alists = [list(a.keys()) for a in ll]
  987. else:
  988. accts = await c.call('listaccounts',0,True)
  989. alists = await c.batch_call('getaddressesbyaccount',[(k,) for k in accts])
  990. return list(zip(accts,alists))
  991. async def add_tw_data(self,wallet):
  992. twd = await self.get_tw_data(wallet)
  993. out,i = {},0
  994. for acct,addr_array in twd:
  995. l = get_obj(TwLabel,proto=self.proto,text=acct,silent=True)
  996. if l and l.mmid.type == 'mmgen':
  997. obj = l.mmid.obj
  998. if len(addr_array) != 1:
  999. die(2,self.msgs['too_many_acct_addresses'].format(acct))
  1000. al_id = AddrListID(SeedID(sid=obj.sid),self.proto.addr_type(obj.mmtype))
  1001. if al_id not in out:
  1002. out[al_id] = []
  1003. out[al_id].append(AddrListEntry(self.proto,idx=obj.idx,addr=addr_array[0],label=l.comment))
  1004. i += 1
  1005. vmsg(f'{i} {pnm} addresses found, {len(twd)} accounts total')
  1006. for al_id in out:
  1007. self.add(AddrList(self.proto,al_id=al_id,adata=AddrListData(sorted(out[al_id],key=lambda a: a.idx))))