json.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. tw.json: export and import tracking wallet to JSON format
  12. """
  13. import sys, os, json
  14. from collections import namedtuple
  15. from ..util import msg, ymsg, fmt, suf, die, make_timestamp, make_chksum_8
  16. from ..base_obj import AsyncInit
  17. from ..objmethods import MMGenObject
  18. from ..rpc import json_encoder
  19. from .ctl import TwCtl
  20. class TwJSON:
  21. class Base(MMGenObject):
  22. can_prune = False
  23. pruned = None
  24. fn_pfx = 'mmgen-tracking-wallet-dump'
  25. def __new__(cls, cfg, proto, *args, **kwargs):
  26. return MMGenObject.__new__(
  27. proto.base_proto_subclass(TwJSON, 'tw.json', sub_clsname=cls.__name__))
  28. def __init__(self, cfg, proto):
  29. self.cfg = cfg
  30. self.proto = proto
  31. self.coin = proto.coin_id.lower()
  32. self.network = proto.network
  33. self.keys = ['mmgen_id', 'address', 'amount', 'comment']
  34. self.entry_tuple = namedtuple('tw_entry', self.keys)
  35. @property
  36. def dump_fn(self):
  37. def get_fn(prune_id):
  38. return '{a}{b}-{c}-{d}.json'.format(
  39. a = self.fn_pfx,
  40. b = f'-pruned[{prune_id}]' if prune_id else '',
  41. c = self.coin,
  42. d = self.network)
  43. if self.pruned:
  44. from ..addrlist import AddrIdxList
  45. prune_id = AddrIdxList(idx_list=self.pruned).id_str
  46. fn = get_fn(prune_id)
  47. mf = 255 if sys.platform == 'win32' else os.statvfs(self.cfg.outdir or os.curdir).f_namemax
  48. if len(fn) > mf:
  49. fn = get_fn(f'idhash={make_chksum_8(prune_id.encode()).lower()}')
  50. else:
  51. fn = get_fn(None)
  52. return fn
  53. def json_dump(self, data, *, pretty=False):
  54. return json.dumps(
  55. data,
  56. cls = json_encoder,
  57. sort_keys = True,
  58. separators = None if pretty else (',', ':'),
  59. indent = 4 if pretty else None) + ('\n' if pretty else '')
  60. def make_chksum(self, data):
  61. return make_chksum_8(self.json_dump(data).encode()).lower()
  62. @property
  63. def mappings_chksum(self):
  64. return self.make_chksum(self.mappings_json)
  65. @property
  66. def entry_tuple_in(self):
  67. return namedtuple('entry_tuple_in', self.keys)
  68. class Import(Base, metaclass=AsyncInit):
  69. blockchain_rescan_warning = None
  70. async def __init__(
  71. self,
  72. cfg,
  73. proto,
  74. filename,
  75. *,
  76. ignore_checksum = False,
  77. batch = False):
  78. super().__init__(cfg, proto)
  79. self.twctl = await TwCtl(cfg, proto, mode='i', rpc_ignore_wallet=True)
  80. def check_network(data):
  81. coin, network = data['network'].split('_')
  82. if coin != self.coin:
  83. die(2, f'Coin in wallet dump is {coin.upper()}, but configured coin is {self.coin.upper()}')
  84. if network != self.network:
  85. die(2, f'Network in wallet dump is {network}, but configured network is {self.network}')
  86. def check_chksum(d):
  87. chksum = self.make_chksum(d['data'])
  88. if chksum != d['checksum']:
  89. if ignore_checksum:
  90. ymsg(f'Warning: ignoring incorrect checksum {chksum}')
  91. else:
  92. die(3, f'File checksum incorrect! ({chksum} != {d["checksum"]})')
  93. def verify_data(d):
  94. check_network(d['data'])
  95. check_chksum(d)
  96. self.cfg._util.compare_or_die(
  97. val1 = self.mappings_chksum,
  98. val2 = d['data']['mappings_checksum'],
  99. desc1 = 'computed mappings checksum',
  100. desc2 = 'saved checksum')
  101. if not await self.check_and_create_wallet():
  102. return
  103. from ..fileutil import get_data_from_file
  104. self.data = json.loads(get_data_from_file(self.cfg, filename, quiet=True))
  105. self.keys = self.data['data']['entries_keys']
  106. self.entries = await self.get_entries()
  107. verify_data(self.data)
  108. addrs = await self.do_import(batch)
  109. await self.twctl.rescan_addresses(addrs)
  110. if self.blockchain_rescan_warning:
  111. ymsg('\n' + fmt(self.blockchain_rescan_warning.strip(), indent=' '))
  112. async def check_and_create_wallet(self):
  113. if await self.tracking_wallet_exists:
  114. die(3,
  115. f'Existing {self.twctl.rpc.daemon.desc} wallet detected!\n' +
  116. 'It must be moved, or backed up and securely deleted, before running this command')
  117. msg('\n'+fmt(self.info_msg.strip(), indent=' '))
  118. from ..ui import keypress_confirm
  119. if not keypress_confirm(self.cfg, 'Continue?'):
  120. msg('Exiting at user request')
  121. return False
  122. if not await self.create_tracking_wallet():
  123. die(3, 'Wallet could not be created')
  124. return True
  125. class Export(Base, metaclass=AsyncInit):
  126. async def __init__(
  127. self,
  128. cfg,
  129. proto,
  130. *,
  131. include_amts = True,
  132. pretty = False,
  133. prune = False,
  134. warn_used = False,
  135. force_overwrite = False):
  136. if prune and not self.can_prune:
  137. die(1, f'Pruning not supported for {proto.name} protocol')
  138. self.prune = prune
  139. self.warn_used = warn_used
  140. super().__init__(cfg, proto)
  141. if not include_amts:
  142. self.keys.remove('amount')
  143. self.twctl = await TwCtl(cfg, proto)
  144. self.entries = await self.get_entries()
  145. if self.prune:
  146. msg('Pruned {} address{}'.format(len(self.pruned), suf(self.pruned, 'es')))
  147. msg('Exporting {} address{}'.format(self.num_entries, suf(self.num_entries, 'es')))
  148. data = {
  149. 'id': 'mmgen_tracking_wallet',
  150. 'version': 1,
  151. 'network': f'{self.coin}_{self.network}',
  152. 'blockheight': self.twctl.rpc.blockcount,
  153. 'time': make_timestamp(),
  154. 'mappings_checksum': self.mappings_chksum,
  155. 'entries_keys': self.keys,
  156. 'entries': await self.entries_out,
  157. 'num_entries': self.num_entries,
  158. }
  159. if include_amts:
  160. data['value'] = await self.total
  161. from ..fileutil import write_data_to_file
  162. write_data_to_file(
  163. cfg = self.cfg,
  164. outfile = self.dump_fn,
  165. data = self.json_dump(
  166. {
  167. 'checksum': self.make_chksum(data),
  168. 'data': data
  169. },
  170. pretty = pretty),
  171. desc = 'tracking wallet JSON data',
  172. ask_overwrite = not force_overwrite)