addr.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. addr: MMGen address-related types
  20. """
  21. from collections import namedtuple
  22. from .objmethods import HiliteStr, InitErrors, MMGenObject
  23. from .obj import ImmutableAttr, MMGenIdx, Int, get_obj
  24. from .seed import SeedID
  25. from . import color as color_mod
  26. ati = namedtuple('addrtype_info',
  27. ['name', 'pubkey_type', 'compressed', 'gen_method', 'addr_fmt', 'wif_label', 'extra_attrs', 'desc'])
  28. class MMGenAddrType(HiliteStr, InitErrors, MMGenObject):
  29. width = 1
  30. trunc_ok = False
  31. color = 'blue'
  32. name = ImmutableAttr(str)
  33. pubkey_type = ImmutableAttr(str)
  34. compressed = ImmutableAttr(bool, set_none_ok=True)
  35. gen_method = ImmutableAttr(str, set_none_ok=True)
  36. addr_fmt = ImmutableAttr(str, set_none_ok=True)
  37. wif_label = ImmutableAttr(str, set_none_ok=True)
  38. extra_attrs = ImmutableAttr(tuple, set_none_ok=True)
  39. desc = ImmutableAttr(str)
  40. pkh_fmts = ('p2pkh', 'bech32', 'ethereum')
  41. mmtypes = {
  42. 'L': ati('legacy', 'std', False,'p2pkh', 'p2pkh', 'wif', (), 'Legacy uncompressed address'),
  43. 'C': ati('compressed','std', True, 'p2pkh', 'p2pkh', 'wif', (), 'Compressed P2PKH address'),
  44. 'S': ati('segwit', 'std', True, 'segwit', 'p2sh', 'wif', (), 'Segwit P2SH-P2WPKH address'),
  45. 'B': ati('bech32', 'std', True, 'bech32', 'bech32', 'wif', (), 'Native Segwit (Bech32) address'),
  46. 'X': ati('bech32x', 'std', True, 'p2pkh', 'bech32', 'wif', (), 'Cross-chain Bech32 address'),
  47. 'E': ati('ethereum', 'std', False,'ethereum','p2pkh', 'privkey', ('wallet_passwd',),'Ethereum address'),
  48. 'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif', ('viewkey',), 'Zcash z-address'),
  49. 'M': ati('monero', 'monero', False,'monero', 'monero', 'spendkey',('viewkey','wallet_passwd'),'Monero address')}
  50. def __new__(cls, proto, id_str, *, errmsg=None):
  51. if isinstance(id_str, cls):
  52. return id_str
  53. try:
  54. id_str = id_str.replace('-', '_')
  55. for k, v in cls.mmtypes.items():
  56. if id_str in (k, v.name):
  57. if id_str == v.name:
  58. id_str = k
  59. me = str.__new__(cls, id_str)
  60. for k in v._fields:
  61. setattr(me, k, getattr(v, k))
  62. if me not in proto.mmtypes + ('P',):
  63. raise ValueError(f'{me.name!r}: invalid address type for {proto.name} protocol')
  64. me.proto = proto
  65. return me
  66. raise ValueError(f'{id_str}: unrecognized address type for protocol {proto.name}')
  67. except Exception as e:
  68. return cls.init_fail(
  69. e,
  70. f"{errmsg or ''}{id_str!r}: invalid value for {cls.__name__} ({e!s})",
  71. preformat = True)
  72. @classmethod
  73. def get_names(cls):
  74. return [v.name for v in cls.mmtypes.values()]
  75. def is_mmgen_addrtype(proto, id_str):
  76. return get_obj(MMGenAddrType, proto=proto, id_str=id_str, silent=True, return_bool=True)
  77. class MMGenPasswordType(MMGenAddrType):
  78. mmtypes = {
  79. 'P': ati('password', 'password', None, None, None, None, None, 'Password generated from MMGen seed')
  80. }
  81. class AddrIdx(MMGenIdx):
  82. max_digits = 7
  83. class MoneroIdx(Int):
  84. max_digits = 5
  85. min_val = 0
  86. def is_addr_idx(s):
  87. return get_obj(AddrIdx, n=s, silent=True, return_bool=True)
  88. class AddrListID(HiliteStr, InitErrors, MMGenObject):
  89. width = 10
  90. trunc_ok = False
  91. color = 'yellow'
  92. def __new__(cls, *, sid=None, mmtype=None, proto=None, id_str=None):
  93. try:
  94. if id_str:
  95. a, b = id_str.split(':')
  96. sid = SeedID(sid=a)
  97. try:
  98. mmtype = MMGenAddrType(proto=proto, id_str=b)
  99. except:
  100. mmtype = MMGenPasswordType(proto=proto, id_str=b)
  101. else:
  102. assert isinstance(sid, SeedID), f'{sid!r} not a SeedID instance'
  103. if not isinstance(mmtype, MMGenAddrType | MMGenPasswordType):
  104. raise ValueError(f'{mmtype!r}: not an instance of MMGenAddrType or MMGenPasswordType')
  105. me = str.__new__(cls, sid+':'+mmtype)
  106. me.sid = sid
  107. me.mmtype = mmtype
  108. return me
  109. except Exception as e:
  110. return cls.init_fail(e, f'sid={sid}, mmtype={mmtype}, id_str={id_str}')
  111. def is_addrlist_id(proto, s):
  112. return get_obj(AddrListID, proto=proto, id_str=s, silent=True, return_bool=True)
  113. class MMGenID(HiliteStr, InitErrors, MMGenObject):
  114. color = 'orange'
  115. width = 0
  116. trunc_ok = False
  117. def __new__(cls, proto, id_str):
  118. try:
  119. match id_str.split(':', 2):
  120. case [sid, mmtype, idx]:
  121. assert mmtype in proto.mmtypes, f'{mmtype}: invalid address type for {proto.cls_name}'
  122. case [sid, idx]:
  123. mmtype = proto.dfl_mmtype
  124. case _:
  125. raise ValueError('not 2 or 3 colon-separated items')
  126. if '-' in idx: # extended Monero ID
  127. assert proto.coin == 'XMR', 'extended MMGen IDs supported for XMR only'
  128. assert id_str.count(':') == 2, 'mmtype letter required for extended MMGen IDs'
  129. me = str.__new__(cls, id_str)
  130. idx, ext = idx.split('-', 1)
  131. me.acct_idx, me.addr_idx = [MoneroIdx(e) for e in ext.split('/', 1)]
  132. me.acct_id = f'{sid}:{mmtype}:{idx}:{me.acct_idx}'
  133. else:
  134. ext = None
  135. me = str.__new__(cls, f'{sid}:{mmtype}:{idx}')
  136. me.sid = SeedID(sid=sid)
  137. me.mmtype = proto.addr_type(mmtype)
  138. me.idx = AddrIdx(idx)
  139. me.al_id = str.__new__(AddrListID, me.sid + ':' + me.mmtype) # checks already done
  140. if ext:
  141. me.acct_sort_key = '{}:{}:{:0{w1}}:{:0{w2}}'.format(
  142. me.sid,
  143. me.mmtype,
  144. me.idx,
  145. me.acct_idx,
  146. w1 = me.idx.max_digits,
  147. w2 = MoneroIdx.max_digits)
  148. me.sort_key = me.acct_sort_key + ':{:0{w2}}'.format(
  149. me.addr_idx,
  150. w2 = MoneroIdx.max_digits)
  151. else:
  152. me.sort_key = '{}:{}:{:0{w}}'.format(me.sid, me.mmtype, me.idx, w=me.idx.max_digits)
  153. me.proto = proto
  154. return me
  155. except Exception as e:
  156. return cls.init_fail(e, id_str)
  157. def is_mmgen_id(proto, s):
  158. return get_obj(MMGenID, proto=proto, id_str=s, silent=True, return_bool=True)
  159. class CoinAddr(HiliteStr, InitErrors, MMGenObject):
  160. color = 'cyan'
  161. hex_width = 40
  162. width = 1
  163. trunc_ok = False
  164. def __new__(cls, proto, addr):
  165. if isinstance(addr, cls):
  166. return addr
  167. try:
  168. ap = proto.decode_addr(addr)
  169. assert ap, f'coin address {addr!r} could not be parsed'
  170. if hasattr(ap, 'addr'):
  171. me = str.__new__(cls, ap.addr)
  172. me.views = ap.views
  173. me.view_pref = ap.view_pref
  174. else:
  175. me = str.__new__(cls, addr)
  176. me.views = [addr]
  177. me.view_pref = 0
  178. me.addr_fmt = ap.fmt
  179. me.bytes = ap.bytes
  180. me.ver_bytes = ap.ver_bytes
  181. me.proto = proto
  182. return me
  183. except Exception as e:
  184. return cls.init_fail(e, addr, objname=f'{proto.name} {proto.cls_name} address')
  185. @property
  186. def parsed(self):
  187. if not hasattr(self, '_parsed'):
  188. self._parsed = self.proto.parse_addr(self.ver_bytes, self.bytes, self.addr_fmt)
  189. return self._parsed
  190. # reimplement some HiliteStr methods:
  191. @classmethod
  192. def fmtc(cls, s, width, /, *, color=False):
  193. return super().fmtc(s[:width-2]+'..' if len(s) > width else s, width, color=color)
  194. def fmt(self, view_pref, width, /, *, color=False):
  195. s = self.views[view_pref]
  196. return super().fmtc(f'{s[:width-2]}..' if len(s) > width else s, width, color=color)
  197. def hl(self, view_pref, /, *, color=True):
  198. return getattr(color_mod, self.color)(self.views[view_pref]) if color else self.views[view_pref]
  199. def is_coin_addr(proto, s):
  200. return get_obj(CoinAddr, proto=proto, addr=s, silent=True, return_bool=True)
  201. class ContractAddr(CoinAddr):
  202. color = 'blue'
  203. def ViewKey(proto, viewkey_str):
  204. return proto.viewkey(viewkey_str)