addr.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
  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. skip_chksum = False ):
  343. self.skip_ka_check = skip_key_address_validity_check
  344. self.update_msgs()
  345. mmtype = mmtype or proto.dfl_mmtype
  346. assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
  347. from .protocol import CoinProtocol
  348. self.bitcoin_addrtypes = tuple(
  349. MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes)
  350. self.proto = proto
  351. do_chksum = False
  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. do_chksum = True
  356. elif addrfile: # data from MMGen address file
  357. self.infile = addrfile
  358. adata = self.parse_file(addrfile) # sets self.al_id
  359. do_chksum = True
  360. elif al_id and adata: # data from tracking wallet
  361. self.al_id = al_id
  362. elif addrlist: # data from flat address list
  363. self.al_id = None
  364. addrlist = remove_dups(addrlist,edesc='address',desc='address list')
  365. adata = AddrListData([AddrListEntry(proto=proto,addr=a) for a in addrlist])
  366. elif keylist: # data from flat key list
  367. self.al_id = None
  368. keylist = remove_dups(keylist,edesc='key',desc='key list',hide=True)
  369. adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in keylist])
  370. elif seed or addr_idxs:
  371. die(3,'Must specify both seed and addr indexes')
  372. elif al_id or adata:
  373. die(3,'Must specify both al_id and adata')
  374. else:
  375. die(3,f'Incorrect arguments for {type(self).__name__}')
  376. # al_id,adata now set
  377. self.data = adata
  378. self.num_addrs = len(adata)
  379. self.fmt_data = ''
  380. self.chksum = None
  381. if self.al_id == None: return
  382. self.id_str = AddrListIDStr(self)
  383. if type(self) == KeyList: return
  384. if do_chksum and not skip_chksum:
  385. self.chksum = AddrListChksum(self)
  386. qmsg(
  387. f'Checksum for {self.data_desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' +
  388. self.msgs[('check_chksum','record_chksum')[src=='gen']] )
  389. def update_msgs(self):
  390. self.msgs = AddrList.msgs
  391. self.msgs.update(type(self).msgs)
  392. def generate(self,seed,addrnums):
  393. assert type(addrnums) is AddrIdxList
  394. seed = self.scramble_seed(seed.data)
  395. dmsg_sc('seed',seed[:8].hex())
  396. compressed = self.al_id.mmtype.compressed
  397. pubkey_type = self.al_id.mmtype.pubkey_type
  398. gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in self.al_id.mmtype.extra_attrs
  399. gen_viewkey = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs
  400. if self.gen_addrs:
  401. kg = KeyGenerator(self.proto,self.al_id.mmtype)
  402. ag = AddrGenerator(self.proto,self.al_id.mmtype)
  403. t_addrs,num,pos,out = len(addrnums),0,0,AddrListData()
  404. le = self.entry_type
  405. while pos != t_addrs:
  406. seed = sha512(seed).digest()
  407. num += 1 # round
  408. if num != addrnums[pos]: continue
  409. pos += 1
  410. if not g.debug:
  411. qmsg_r(f'\rGenerating {self.gen_desc} #{num} ({pos} of {t_addrs})')
  412. e = le(proto=self.proto,idx=num)
  413. # Secret key is double sha256 of seed hash round /num/
  414. e.sec = PrivKey(
  415. self.proto,
  416. sha256(sha256(seed).digest()).digest(),
  417. compressed = compressed,
  418. pubkey_type = pubkey_type )
  419. if self.gen_addrs:
  420. pubhex = kg.to_pubhex(e.sec)
  421. e.addr = ag.to_addr(pubhex)
  422. if gen_viewkey:
  423. e.viewkey = ag.to_viewkey(pubhex)
  424. if gen_wallet_passwd:
  425. e.wallet_passwd = ag.to_wallet_passwd(e.sec)
  426. if type(self) == PasswordList:
  427. e.passwd = str(self.make_passwd(e.sec)) # TODO - own type
  428. dmsg(f'Key {pos:>03}: {e.passwd}')
  429. out.append(e)
  430. if g.debug_addrlist:
  431. Msg(f'generate():\n{e.pfmt()}')
  432. qmsg('\r{}: {} {}{} generated{}'.format(
  433. self.al_id.hl(),
  434. t_addrs,
  435. self.gen_desc,
  436. suf(t_addrs,self.gen_desc_pl),
  437. ' ' * 15 ))
  438. return out
  439. def check_format(self,addr):
  440. return True # format is checked when added to list entry object
  441. def scramble_seed(self,seed):
  442. is_btcfork = self.proto.base_coin == 'BTC'
  443. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  444. dmsg_sc('str','(none)')
  445. return seed
  446. if self.proto.base_coin == 'ETH':
  447. scramble_key = self.proto.coin.lower()
  448. else:
  449. scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  450. from .crypto import scramble_seed
  451. if self.proto.testnet:
  452. scramble_key += ':' + self.proto.network
  453. dmsg_sc('str',scramble_key)
  454. return scramble_seed(seed,scramble_key.encode())
  455. def encrypt(self,desc='new key list'):
  456. from .crypto import mmgen_encrypt
  457. self.fmt_data = mmgen_encrypt(self.fmt_data.encode(),desc,'')
  458. self.ext += '.'+g.mmenc_ext
  459. def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
  460. tn = ('.' + self.proto.network) if self.proto.testnet else ''
  461. fn = '{}{x}{}.{}'.format(self.id_str,tn,self.ext,x='-α' if g.debug_utf8 else '')
  462. ask_tty = self.has_keys and not opt.quiet
  463. write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary)
  464. def idxs(self):
  465. return [e.idx for e in self.data]
  466. def addrs(self):
  467. return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
  468. def addrpairs(self):
  469. return [(e.idx,e.addr) for e in self.data]
  470. def coinaddrs(self):
  471. return [e.addr for e in self.data]
  472. def comments(self):
  473. return [e.label for e in self.data]
  474. def entry(self,idx):
  475. for e in self.data:
  476. if idx == e.idx: return e
  477. def coinaddr(self,idx):
  478. for e in self.data:
  479. if idx == e.idx: return e.addr
  480. def comment(self,idx):
  481. for e in self.data:
  482. if idx == e.idx: return e.label
  483. def set_comment(self,idx,comment):
  484. for e in self.data:
  485. if idx == e.idx:
  486. e.label = comment
  487. def make_reverse_dict_addrlist(self,coinaddrs):
  488. d = MMGenDict()
  489. b = coinaddrs
  490. for e in self.data:
  491. try:
  492. d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.label )
  493. except ValueError:
  494. pass
  495. return d
  496. def remove_dup_keys(self,cmplist):
  497. assert self.has_keys
  498. pop_list = []
  499. for n,d in enumerate(self.data):
  500. for e in cmplist.data:
  501. if e.sec.wif == d.sec.wif:
  502. pop_list.append(n)
  503. for n in reversed(pop_list): self.data.pop(n)
  504. if pop_list:
  505. vmsg(self.msgs['removed_dup_keys'].format(len(pop_list),suf(removed)))
  506. def add_wifs(self,key_list):
  507. if not key_list: return
  508. for d in self.data:
  509. for e in key_list.data:
  510. if e.addr and e.sec and e.addr == d.addr:
  511. d.sec = e.sec
  512. def list_missing(self,key):
  513. return [d.addr for d in self.data if not getattr(d,key)]
  514. def generate_addrs_from_keys(self):
  515. # assume that the first listed mmtype is valid for flat key list
  516. at = self.proto.addr_type(self.proto.mmtypes[0])
  517. kg = KeyGenerator(self.proto,at.pubkey_type)
  518. ag = AddrGenerator(self.proto,at)
  519. d = self.data
  520. for n,e in enumerate(d,1):
  521. qmsg_r(f'\rGenerating addresses from keylist: {n}/{len(d)}')
  522. e.addr = ag.to_addr(kg.to_pubhex(e.sec))
  523. if g.debug_addrlist:
  524. Msg(f'generate_addrs_from_keys():\n{e.pfmt()}')
  525. qmsg(f'\rGenerated addresses from keylist: {n}/{len(d)} ')
  526. def make_label(self):
  527. bc,mt = self.proto.base_coin,self.al_id.mmtype
  528. l_coin = [] if bc == 'BTC' else [self.proto.coin] if bc == 'ETH' else [bc]
  529. l_type = [] if mt == 'E' or (mt == 'L' and not self.proto.testnet) else [mt.name.upper()]
  530. l_tn = [] if not self.proto.testnet else [self.proto.network.upper()]
  531. lbl_p2 = ':'.join(l_coin+l_type+l_tn)
  532. return self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2
  533. def format(self,add_comments=False):
  534. out = [self.msgs['file_header']+'\n']
  535. if self.chksum:
  536. out.append(f'# {capfirst(self.data_desc)} data checksum for {self.id_str}: {self.chksum}')
  537. out.append('# Record this value to a secure location.\n')
  538. lbl = self.make_label()
  539. dmsg_sc('lbl',lbl[9:])
  540. out.append(f'{lbl} {{')
  541. fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx))
  542. for e in self.data:
  543. c = ' '+e.label if add_comments and e.label else ''
  544. if type(self) == KeyList:
  545. out.append(fs.format( e.idx, f'{self.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
  546. elif type(self) == PasswordList:
  547. out.append(fs.format(e.idx,e.passwd,c))
  548. else: # First line with idx
  549. out.append(fs.format(e.idx,e.addr,c))
  550. if self.has_keys:
  551. if opt.b16:
  552. out.append(fs.format( '', f'orig_hex: {e.sec.orig_hex}', c ))
  553. out.append(fs.format( '', f'{self.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
  554. for k in ('viewkey','wallet_passwd'):
  555. v = getattr(e,k)
  556. if v: out.append(fs.format( '', f'{k}: {v}', c ))
  557. out.append('}')
  558. self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
  559. def get_line(self,lines):
  560. ret = lines.pop(0).split(None,2)
  561. self.line_ctr += 1
  562. if ret[0] == 'orig_hex:': # hacky
  563. ret = lines.pop(0).split(None,2)
  564. self.line_ctr += 1
  565. return ret if len(ret) == 3 else ret + ['']
  566. def parse_file_body(self,lines):
  567. ret = AddrListData()
  568. le = self.entry_type
  569. iifs = "{!r}: invalid identifier [expected '{}:']"
  570. while lines:
  571. idx,addr,lbl = self.get_line(lines)
  572. assert is_mmgen_idx(idx), f'invalid address index {idx!r}'
  573. self.check_format(addr)
  574. a = le(**{ 'proto': self.proto, 'idx':int(idx), self.main_attr:addr, 'label':lbl })
  575. if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
  576. d = self.get_line(lines)
  577. assert d[0] == self.al_id.mmtype.wif_label+':', iifs.format(d[0],self.al_id.mmtype.wif_label)
  578. a.sec = PrivKey(proto=self.proto,wif=d[1])
  579. for k,dtype,add_proto in (
  580. ('viewkey',ViewKey,True),
  581. ('wallet_passwd',WalletPassword,False) ):
  582. if k in self.al_id.mmtype.extra_attrs:
  583. d = self.get_line(lines)
  584. assert d[0] == k+':', iifs.format(d[0],k)
  585. setattr(a,k,dtype( *((self.proto,d[1]) if add_proto else (d[1],)) ) )
  586. ret.append(a)
  587. if self.has_keys and not self.skip_ka_check:
  588. if getattr(opt,'yes',False) or keypress_confirm('Check key-to-address validity?'):
  589. kg = KeyGenerator(self.proto,self.al_id.mmtype)
  590. ag = AddrGenerator(self.proto,self.al_id.mmtype)
  591. llen = len(ret)
  592. for n,e in enumerate(ret):
  593. qmsg_r(f'\rVerifying keys {n+1}/{llen}')
  594. assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),(
  595. f'Key doesn’t match address!\n {e.sec.wif}\n {e.addr}')
  596. qmsg(' - done')
  597. return ret
  598. def parse_file(self,fn,buf=[],exit_on_error=True):
  599. def parse_addrfile_label(lbl):
  600. """
  601. label examples:
  602. - Bitcoin legacy mainnet: no label
  603. - Bitcoin legacy testnet: 'LEGACY:TESTNET'
  604. - Bitcoin Segwit: 'SEGWIT'
  605. - Bitcoin Segwit testnet: 'SEGWIT:TESTNET'
  606. - Bitcoin Bech32 regtest: 'BECH32:REGTEST'
  607. - Litecoin legacy mainnet: 'LTC'
  608. - Litecoin Bech32 mainnet: 'LTC:BECH32'
  609. - Litecoin legacy testnet: 'LTC:LEGACY:TESTNET'
  610. - Ethereum mainnet: 'ETH'
  611. - Ethereum Classic mainnet: 'ETC'
  612. - Ethereum regtest: 'ETH:REGTEST'
  613. """
  614. lbl = lbl.lower()
  615. # remove the network component:
  616. if lbl.endswith(':testnet'):
  617. network = 'testnet'
  618. lbl = lbl[:-8]
  619. elif lbl.endswith(':regtest'):
  620. network = 'regtest'
  621. lbl = lbl[:-8]
  622. else:
  623. network = 'mainnet'
  624. if lbl in self.bitcoin_addrtypes:
  625. coin,mmtype_key = ( 'BTC', lbl )
  626. elif ':' in lbl: # first component is coin, second is mmtype_key
  627. coin,mmtype_key = lbl.split(':')
  628. else: # only component is coin
  629. coin,mmtype_key = ( lbl, None )
  630. proto = init_proto(coin=coin,network=network)
  631. if mmtype_key == None:
  632. mmtype_key = proto.mmtypes[0]
  633. return ( proto, proto.addr_type(mmtype_key) )
  634. lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
  635. try:
  636. assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})'
  637. ls = lines[0].split()
  638. assert 1 < len(ls) < 5, f'Invalid first line for {self.gen_desc} file: {lines[0]!r}'
  639. assert ls.pop() == '{', f'{ls!r}: invalid first line'
  640. assert lines[-1] == '}', f'{lines[-1]!r}: invalid last line'
  641. sid = ls.pop(0)
  642. assert is_mmgen_seed_id(sid), f'{sid!r}: invalid Seed ID'
  643. if type(self) == PasswordList and len(ls) == 2:
  644. ss = ls.pop().split(':')
  645. assert len(ss) == 2, f'{ss!r}: invalid password length specifier (must contain colon)'
  646. self.set_pw_fmt(ss[0])
  647. self.set_pw_len(ss[1])
  648. self.pw_id_str = MMGenPWIDString(ls.pop())
  649. proto = init_proto('btc')# FIXME: dummy protocol
  650. mmtype = MMGenPasswordType(proto,'P')
  651. elif len(ls) == 1:
  652. proto,mmtype = parse_addrfile_label(ls[0])
  653. elif len(ls) == 0:
  654. proto = init_proto('btc')
  655. mmtype = proto.addr_type('L')
  656. else:
  657. raise ValueError(f'{lines[0]}: Invalid first line for {self.gen_desc} file {fn!r}')
  658. if type(self) != PasswordList:
  659. if proto.base_coin != self.proto.base_coin or proto.network != self.proto.network:
  660. """
  661. Having caller supply protocol and checking address file protocol against it here
  662. allows us to catch all mismatches in one place. This behavior differs from that of
  663. transaction files, which determine the protocol independently, requiring the caller
  664. to check for protocol mismatches (e.g. MMGenTX.check_correct_chain())
  665. """
  666. raise ValueError(
  667. f'{self.data_desc} file is '
  668. + f'{proto.base_coin} {proto.network} but protocol is '
  669. + f'{self.proto.base_coin} {self.proto.network}' )
  670. self.base_coin = proto.base_coin
  671. self.network = proto.network
  672. self.al_id = AddrListID(SeedID(sid=sid),mmtype)
  673. data = self.parse_file_body(lines[1:-1])
  674. assert isinstance(data,list),'Invalid file body data'
  675. except Exception as e:
  676. m = 'Invalid data in {} list file {!r}{} ({!s})'.format(
  677. self.data_desc,
  678. self.infile,
  679. (f', content line {self.line_ctr}' if self.line_ctr else ''),
  680. e )
  681. if exit_on_error:
  682. die(3,m)
  683. else:
  684. msg(m)
  685. return False
  686. return data
  687. class KeyAddrList(AddrList):
  688. data_desc = 'key-address'
  689. file_desc = 'secret keys'
  690. gen_desc = 'key/address pair'
  691. gen_desc_pl = 's'
  692. gen_addrs = True
  693. gen_keys = True
  694. has_keys = True
  695. ext = 'akeys'
  696. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  697. class KeyList(AddrList):
  698. msgs = {
  699. 'file_header': f"""
  700. # {pnm} key file
  701. #
  702. # This file is editable.
  703. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  704. """.strip()
  705. }
  706. data_desc = 'key'
  707. file_desc = 'secret keys'
  708. gen_desc = 'key'
  709. gen_desc_pl = 's'
  710. gen_addrs = False
  711. gen_keys = True
  712. has_keys = True
  713. ext = 'keys'
  714. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  715. def is_bip39_str(s):
  716. from .bip39 import bip39
  717. return bool(bip39.tohex(s.split(),wl_id='bip39'))
  718. def is_xmrseed(s):
  719. return bool(baseconv.tobytes(s.split(),wl_id='xmrseed'))
  720. from collections import namedtuple
  721. class PasswordList(AddrList):
  722. msgs = {
  723. 'file_header': f"""
  724. # {pnm} password file
  725. #
  726. # This file is editable.
  727. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  728. # A text label of {TwComment.max_screen_width} screen cells or less may be added to the right of each
  729. # password. The label may contain any printable ASCII symbol.
  730. #
  731. """.strip(),
  732. 'file_header_mn': f"""
  733. # {pnm} {{}} password file
  734. #
  735. # This file is editable.
  736. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  737. #
  738. """.strip(),
  739. 'record_chksum': """
  740. Record this checksum: it will be used to verify the password file in the future
  741. """.strip()
  742. }
  743. entry_type = PasswordListEntry
  744. main_attr = 'passwd'
  745. data_desc = 'password'
  746. file_desc = 'passwords'
  747. gen_desc = 'password'
  748. gen_desc_pl = 's'
  749. gen_addrs = False
  750. gen_keys = False
  751. gen_passwds = True
  752. has_keys = False
  753. ext = 'pws'
  754. pw_len = None
  755. dfl_pw_fmt = 'b58'
  756. pwinfo = namedtuple('passwd_info',['min_len','max_len','dfl_len','valid_lens','desc','chk_func'])
  757. pw_info = {
  758. 'b32': pwinfo(10, 42 ,24, None, 'base32 password', is_b32_str), # 32**24 < 2**128
  759. 'b58': pwinfo(8, 36 ,20, None, 'base58 password', is_b58_str), # 58**20 < 2**128
  760. 'bip39': pwinfo(12, 24 ,24, [12,18,24],'BIP39 mnemonic', is_bip39_str),
  761. 'xmrseed': pwinfo(25, 25, 25, [25], 'Monero new-style mnemonic',is_xmrseed),
  762. 'hex': pwinfo(32, 64 ,64, [32,48,64],'hexadecimal password', is_hex_str),
  763. }
  764. chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
  765. feature_warn_fs = 'WARNING: {!r} is a potentially dangerous feature. Use at your own risk!'
  766. hex2bip39 = False
  767. def __init__(self,proto,
  768. infile = None,
  769. seed = None,
  770. pw_idxs = None,
  771. pw_id_str = None,
  772. pw_len = None,
  773. pw_fmt = None,
  774. chk_params_only = False
  775. ):
  776. self.proto = proto # proto is ignored
  777. self.update_msgs()
  778. if infile:
  779. self.infile = infile
  780. self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
  781. else:
  782. if not chk_params_only:
  783. for k in (seed,pw_idxs):
  784. assert k
  785. self.pw_id_str = MMGenPWIDString(pw_id_str)
  786. self.set_pw_fmt(pw_fmt)
  787. self.set_pw_len(pw_len)
  788. if chk_params_only:
  789. return
  790. if self.hex2bip39:
  791. ymsg(self.feature_warn_fs.format(pw_fmt))
  792. self.set_pw_len_vs_seed_len(pw_len,seed)
  793. self.al_id = AddrListID(seed.sid,MMGenPasswordType(self.proto,'P'))
  794. self.data = self.generate(seed,pw_idxs)
  795. if self.pw_fmt in ('bip39','xmrseed'):
  796. self.msgs['file_header'] = self.msgs['file_header_mn'].format(self.pw_fmt.upper())
  797. self.num_addrs = len(self.data)
  798. self.fmt_data = ''
  799. self.chksum = AddrListChksum(self)
  800. fs = f'{self.al_id.sid}-{self.pw_id_str}-{self.pw_fmt_disp}-{self.pw_len}[{{}}]'
  801. self.id_str = AddrListIDStr(self,fs)
  802. qmsg(
  803. f'Checksum for {self.data_desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' +
  804. self.msgs[('record_chksum','check_chksum')[bool(infile)]] )
  805. def set_pw_fmt(self,pw_fmt):
  806. if pw_fmt == 'hex2bip39':
  807. self.hex2bip39 = True
  808. self.pw_fmt = 'bip39'
  809. self.pw_fmt_disp = 'hex2bip39'
  810. else:
  811. self.pw_fmt = pw_fmt
  812. self.pw_fmt_disp = pw_fmt
  813. if self.pw_fmt not in self.pw_info:
  814. raise InvalidPasswdFormat(
  815. '{!r}: invalid password format. Valid formats: {}'.format(
  816. self.pw_fmt,
  817. ', '.join(self.pw_info) ))
  818. def chk_pw_len(self,passwd=None):
  819. if passwd is None:
  820. assert self.pw_len,'either passwd or pw_len must be set'
  821. pw_len = self.pw_len
  822. fs = '{l}: invalid user-requested length for {b} ({c}{m})'
  823. else:
  824. pw_len = len(passwd)
  825. fs = '{pw}: {b} has invalid length {l} ({c}{m} characters)'
  826. d = self.pw_info[self.pw_fmt]
  827. if d.valid_lens:
  828. if pw_len not in d.valid_lens:
  829. die(2, fs.format( l=pw_len, b=d.desc, c='not one of ', m=d.valid_lens, pw=passwd ))
  830. elif pw_len > d.max_len:
  831. die(2, fs.format( l=pw_len, b=d.desc, c='>', m=d.max_len, pw=passwd ))
  832. elif pw_len < d.min_len:
  833. die(2, fs.format( l=pw_len, b=d.desc, c='<', m=d.min_len, pw=passwd ))
  834. def set_pw_len(self,pw_len):
  835. d = self.pw_info[self.pw_fmt]
  836. if pw_len is None:
  837. self.pw_len = d.dfl_len
  838. return
  839. if not is_int(pw_len):
  840. die(2,f'{pw_len!r}: invalid user-requested password length (not an integer)')
  841. self.pw_len = int(pw_len)
  842. self.chk_pw_len()
  843. def set_pw_len_vs_seed_len(self,pw_len,seed):
  844. pf = self.pw_fmt
  845. if pf == 'hex':
  846. pw_bytes = self.pw_len // 2
  847. good_pw_len = seed.byte_len * 2
  848. elif pf == 'bip39':
  849. from .bip39 import bip39
  850. pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True)
  851. good_pw_len = bip39.seedlen2nwords(seed.byte_len,in_bytes=True)
  852. elif pf == 'xmrseed':
  853. pw_bytes = baseconv.seedlen_map_rev['xmrseed'][self.pw_len]
  854. try:
  855. good_pw_len = baseconv.seedlen_map['xmrseed'][seed.byte_len]
  856. except:
  857. die(1,f'{seed.byte_len*8}: unsupported seed length for Monero new-style mnemonic')
  858. elif pf in ('b32','b58'):
  859. pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
  860. pw_bytes = pw_int.bit_length() // 8
  861. good_pw_len = len(baseconv.frombytes(b'\xff'*seed.byte_len,wl_id=pf))
  862. else:
  863. raise NotImplementedError(f'{pf!r}: unknown password format')
  864. if pw_bytes > seed.byte_len:
  865. die(1,
  866. 'Cannot generate passwords with more entropy than underlying seed! ({} bits)\n'.format(
  867. len(seed.data) * 8 ) + (
  868. 'Re-run the command with --passwd-len={}' if pf in ('bip39','hex') else
  869. 'Re-run the command, specifying a password length of {} or less'
  870. ).format(good_pw_len) )
  871. if pf in ('bip39','hex') and pw_bytes < seed.byte_len:
  872. if not keypress_confirm(
  873. f'WARNING: requested {self.pw_info[pf].desc} length has less entropy ' +
  874. 'than underlying seed!\nIs this what you want?',
  875. default_yes = True ):
  876. die(1,'Exiting at user request')
  877. def make_passwd(self,hex_sec):
  878. assert self.pw_fmt in self.pw_info
  879. if self.pw_fmt == 'hex':
  880. # take most significant part
  881. return hex_sec[:self.pw_len]
  882. elif self.pw_fmt == 'bip39':
  883. from .bip39 import bip39
  884. pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True)
  885. # take most significant part
  886. return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39'))
  887. elif self.pw_fmt == 'xmrseed':
  888. pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2
  889. # take most significant part
  890. bytes_trunc = bytes.fromhex(hex_sec[:pw_len_hex])
  891. bytes_preproc = init_proto('xmr').preprocess_key(bytes_trunc,None)
  892. return ' '.join(baseconv.frombytes(bytes_preproc,wl_id='xmrseed'))
  893. else:
  894. # take least significant part
  895. return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
  896. def check_format(self,pw):
  897. if not self.pw_info[self.pw_fmt].chk_func(pw):
  898. raise ValueError(f'Password is not valid {self.pw_info[self.pw_fmt].desc} data')
  899. pwlen = len(pw.split()) if self.pw_fmt in ('bip39','xmrseed') else len(pw)
  900. if pwlen != self.pw_len:
  901. raise ValueError(f'Password has incorrect length ({pwlen} != {self.pw_len})')
  902. return True
  903. def scramble_seed(self,seed):
  904. # Changing either pw_fmt or pw_len will cause a different, unrelated
  905. # set of passwords to be generated: this is what we want.
  906. # NB: In original implementation, pw_id_str was 'baseN', not 'bN'
  907. scramble_key = f'{self.pw_fmt}:{self.pw_len}:{self.pw_id_str}'
  908. if self.hex2bip39:
  909. from .bip39 import bip39
  910. pwlen = bip39.nwords2seedlen(self.pw_len,in_hex=True)
  911. scramble_key = f'hex:{pwlen}:{self.pw_id_str}'
  912. from .crypto import scramble_seed
  913. dmsg_sc('str',scramble_key)
  914. return scramble_seed(seed,scramble_key.encode())
  915. def get_line(self,lines):
  916. self.line_ctr += 1
  917. if self.pw_fmt in ('bip39','xmrseed'):
  918. ret = lines.pop(0).split(None,self.pw_len+1)
  919. if len(ret) > self.pw_len+1:
  920. m1 = f'extraneous text {ret[self.pw_len+1]!r} found after password'
  921. m2 = '[bare comments not allowed in BIP39 password files]'
  922. m = m1+' '+m2
  923. elif len(ret) < self.pw_len+1:
  924. m = f'invalid password length {len(ret)-1}'
  925. else:
  926. return (ret[0],' '.join(ret[1:self.pw_len+1]),'')
  927. raise ValueError(m)
  928. else:
  929. ret = lines.pop(0).split(None,2)
  930. return ret if len(ret) == 3 else ret + ['']
  931. def make_label(self):
  932. return f'{self.al_id.sid} {self.pw_id_str} {self.pw_fmt_disp}:{self.pw_len}'
  933. class AddrData(MMGenObject):
  934. msgs = {
  935. 'too_many_acct_addresses': f"""
  936. ERROR: More than one address found for account: '{{}}'.
  937. Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
  938. Please restore your tracking wallet from a backup or create a new one and
  939. re-import your addresses.
  940. """.strip()
  941. }
  942. def __new__(cls,proto,*args,**kwargs):
  943. return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
  944. def __init__(self,proto,*args,**kwargs):
  945. self.al_ids = {}
  946. self.proto = proto
  947. self.rpc = None
  948. def seed_ids(self):
  949. return list(self.al_ids.keys())
  950. def addrlist(self,al_id):
  951. # TODO: Validate al_id
  952. if al_id in self.al_ids:
  953. return self.al_ids[al_id]
  954. def mmaddr2coinaddr(self,mmaddr):
  955. al_id,idx = MMGenID(self.proto,mmaddr).rsplit(':',1)
  956. coinaddr = ''
  957. if al_id in self.al_ids:
  958. coinaddr = self.addrlist(al_id).coinaddr(int(idx))
  959. return coinaddr or None
  960. def coinaddr2mmaddr(self,coinaddr):
  961. d = self.make_reverse_dict([coinaddr])
  962. return (list(d.values())[0][0]) if d else None
  963. def add(self,addrlist):
  964. if type(addrlist) == AddrList:
  965. self.al_ids[addrlist.al_id] = addrlist
  966. return True
  967. else:
  968. raise TypeError(f'Error: object {addrlist!r} is not of type AddrList')
  969. def make_reverse_dict(self,coinaddrs):
  970. d = MMGenDict()
  971. for al_id in self.al_ids:
  972. d.update(self.al_ids[al_id].make_reverse_dict_addrlist(coinaddrs))
  973. return d
  974. class TwAddrData(AddrData,metaclass=AsyncInit):
  975. def __new__(cls,proto,*args,**kwargs):
  976. return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
  977. async def __init__(self,proto,wallet=None):
  978. self.proto = proto
  979. from .rpc import rpc_init
  980. self.rpc = await rpc_init(proto)
  981. self.al_ids = {}
  982. await self.add_tw_data(wallet)
  983. async def get_tw_data(self,wallet=None):
  984. vmsg('Getting address data from tracking wallet')
  985. c = self.rpc
  986. if 'label_api' in c.caps:
  987. accts = await c.call('listlabels')
  988. ll = await c.batch_call('getaddressesbylabel',[(k,) for k in accts])
  989. alists = [list(a.keys()) for a in ll]
  990. else:
  991. accts = await c.call('listaccounts',0,True)
  992. alists = await c.batch_call('getaddressesbyaccount',[(k,) for k in accts])
  993. return list(zip(accts,alists))
  994. async def add_tw_data(self,wallet):
  995. twd = await self.get_tw_data(wallet)
  996. out,i = {},0
  997. for acct,addr_array in twd:
  998. l = get_obj(TwLabel,proto=self.proto,text=acct,silent=True)
  999. if l and l.mmid.type == 'mmgen':
  1000. obj = l.mmid.obj
  1001. if len(addr_array) != 1:
  1002. die(2,self.msgs['too_many_acct_addresses'].format(acct))
  1003. al_id = AddrListID(SeedID(sid=obj.sid),self.proto.addr_type(obj.mmtype))
  1004. if al_id not in out:
  1005. out[al_id] = []
  1006. out[al_id].append(AddrListEntry(self.proto,idx=obj.idx,addr=addr_array[0],label=l.comment))
  1007. i += 1
  1008. vmsg(f'{i} {pnm} addresses found, {len(twd)} accounts total')
  1009. for al_id in out:
  1010. self.add(AddrList(self.proto,al_id=al_id,adata=AddrListData(sorted(out[al_id],key=lambda a: a.idx))))