tw.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 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. altcoins.eth.tw: Ethereum tracking wallet and related classes for the MMGen suite
  20. """
  21. from mmgen.common import *
  22. from mmgen.obj import TwLabel,is_coin_addr,is_mmgen_id,ListItemAttr,ImmutableAttr
  23. from mmgen.tw import TrackingWallet,TwAddrList,TwUnspentOutputs,TwGetBalance
  24. from mmgen.addrdata import AddrData,TwAddrData
  25. from .contract import Token,TokenResolve
  26. from .obj import ETHAmt
  27. class EthereumTrackingWallet(TrackingWallet):
  28. caps = ('batch',)
  29. data_key = 'accounts'
  30. use_tw_file = True
  31. async def is_in_wallet(self,addr):
  32. return addr in self.data_root
  33. def init_empty(self):
  34. self.data = { 'coin': self.proto.coin, 'accounts': {}, 'tokens': {} }
  35. def upgrade_wallet_maybe(self):
  36. upgraded = False
  37. if not 'accounts' in self.data or not 'coin' in self.data:
  38. ymsg(f'Upgrading {self.desc} (v1->v2: accounts field added)')
  39. if not 'accounts' in self.data:
  40. self.data = {}
  41. import json
  42. self.data['accounts'] = json.loads(self.orig_data)
  43. if not 'coin' in self.data:
  44. self.data['coin'] = self.proto.coin
  45. upgraded = True
  46. def have_token_params_fields():
  47. for k in self.data['tokens']:
  48. if 'params' in self.data['tokens'][k]:
  49. return True
  50. def add_token_params_fields():
  51. for k in self.data['tokens']:
  52. self.data['tokens'][k]['params'] = {}
  53. if not 'tokens' in self.data:
  54. self.data['tokens'] = {}
  55. upgraded = True
  56. if self.data['tokens'] and not have_token_params_fields():
  57. ymsg(f'Upgrading {self.desc} (v2->v3: token params fields added)')
  58. add_token_params_fields()
  59. upgraded = True
  60. if upgraded:
  61. self.force_write()
  62. msg(f'{self.desc} upgraded successfully!')
  63. async def rpc_get_balance(self,addr):
  64. return ETHAmt(int(await self.rpc.call('eth_getBalance','0x'+addr,'latest'),16),'wei')
  65. @write_mode
  66. async def batch_import_address(self,args_list):
  67. for arg_list in args_list:
  68. await self.import_address(*arg_list)
  69. return args_list
  70. @write_mode
  71. async def import_address(self,addr,label,foo):
  72. r = self.data_root
  73. if addr in r:
  74. if not r[addr]['mmid'] and label.mmid:
  75. msg(f'Warning: MMGen ID {label.mmid!r} was missing in tracking wallet!')
  76. elif r[addr]['mmid'] != label.mmid:
  77. die(3,'MMGen ID {label.mmid!r} does not match tracking wallet!')
  78. r[addr] = { 'mmid': label.mmid, 'comment': label.comment }
  79. @write_mode
  80. async def remove_address(self,addr):
  81. r = self.data_root
  82. if is_coin_addr(self.proto,addr):
  83. have_match = lambda k: k == addr
  84. elif is_mmgen_id(self.proto,addr):
  85. have_match = lambda k: r[k]['mmid'] == addr
  86. else:
  87. die(1,f'{addr!r} is not an Ethereum address or MMGen ID')
  88. for k in r:
  89. if have_match(k):
  90. # return the addr resolved to mmid if possible
  91. ret = r[k]['mmid'] if is_mmgen_id(self.proto,r[k]['mmid']) else addr
  92. del r[k]
  93. self.write()
  94. return ret
  95. else:
  96. msg(f'Address {addr!r} not found in {self.data_root_desc!r} section of tracking wallet')
  97. return None
  98. @write_mode
  99. async def set_label(self,coinaddr,lbl):
  100. for addr,d in list(self.data_root.items()):
  101. if addr == coinaddr:
  102. d['comment'] = lbl.comment
  103. self.write()
  104. return None
  105. else:
  106. msg(f'Address {coinaddr!r} not found in {self.data_root_desc!r} section of tracking wallet')
  107. return False
  108. async def addr2sym(self,req_addr):
  109. for addr in self.data['tokens']:
  110. if addr == req_addr:
  111. return self.data['tokens'][addr]['params']['symbol']
  112. else:
  113. return None
  114. async def sym2addr(self,sym):
  115. for addr in self.data['tokens']:
  116. if self.data['tokens'][addr]['params']['symbol'] == sym.upper():
  117. return addr
  118. else:
  119. return None
  120. def get_token_param(self,token,param):
  121. if token in self.data['tokens']:
  122. return self.data['tokens'][token]['params'].get(param)
  123. return None
  124. class EthereumTokenTrackingWallet(EthereumTrackingWallet):
  125. desc = 'Ethereum token tracking wallet'
  126. decimals = None
  127. symbol = None
  128. cur_eth_balances = {}
  129. async def __init__(self,proto,mode='r',token_addr=None):
  130. await super().__init__(proto,mode=mode)
  131. for v in self.data['tokens'].values():
  132. self.conv_types(v)
  133. if self.importing and token_addr:
  134. if not is_coin_addr(proto,token_addr):
  135. raise InvalidTokenAddress(f'{token_addr!r}: invalid token address')
  136. else:
  137. assert token_addr == None,'EthereumTokenTrackingWallet_chk1'
  138. token_addr = await self.sym2addr(proto.tokensym) # returns None on failure
  139. if not is_coin_addr(proto,token_addr):
  140. raise UnrecognizedTokenSymbol(f'Specified token {proto.tokensym!r} could not be resolved!')
  141. from mmgen.obj import TokenAddr
  142. self.token = TokenAddr(proto,token_addr)
  143. if self.token not in self.data['tokens']:
  144. if self.importing:
  145. await self.import_token(self.token)
  146. else:
  147. raise TokenNotInWallet(f'Specified token {self.token!r} not in wallet!')
  148. self.decimals = self.get_param('decimals')
  149. self.symbol = self.get_param('symbol')
  150. proto.tokensym = self.symbol
  151. async def is_in_wallet(self,addr):
  152. return addr in self.data['tokens'][self.token]
  153. @property
  154. def data_root(self):
  155. return self.data['tokens'][self.token]
  156. @property
  157. def data_root_desc(self):
  158. return 'token ' + self.get_param('symbol')
  159. async def rpc_get_balance(self,addr):
  160. return await Token(self.proto,self.token,self.decimals,self.rpc).get_balance(addr)
  161. async def get_eth_balance(self,addr,force_rpc=False):
  162. cache = self.cur_eth_balances
  163. r = self.data['accounts']
  164. ret = None if force_rpc else self.get_cached_balance(addr,cache,r)
  165. if ret == None:
  166. ret = await super().rpc_get_balance(addr)
  167. self.cache_balance(addr,ret,cache,r)
  168. return ret
  169. def get_param(self,param):
  170. return self.data['tokens'][self.token]['params'][param]
  171. @write_mode
  172. async def import_token(self,tokenaddr):
  173. """
  174. Token 'symbol' and 'decimals' values are resolved from the network by the system just
  175. once, upon token import. Thereafter, token address, symbol and decimals are resolved
  176. either from the tracking wallet (online operations) or transaction file (when signing).
  177. """
  178. t = await TokenResolve(self.proto,self.rpc,tokenaddr)
  179. self.data['tokens'][tokenaddr] = {
  180. 'params': {
  181. 'symbol': await t.get_symbol(),
  182. 'decimals': t.decimals
  183. }
  184. }
  185. # No unspent outputs with Ethereum, but naming must be consistent
  186. class EthereumTwUnspentOutputs(TwUnspentOutputs):
  187. disp_type = 'eth'
  188. can_group = False
  189. col_adj = 29
  190. hdr_fmt = 'TRACKED ACCOUNTS (sort order: {})\nTotal {}: {}'
  191. desc = 'account balances'
  192. item_desc = 'account'
  193. dump_fn_pfx = 'balances'
  194. prompt = """
  195. Sort options: [a]mount, a[d]dress, [r]everse, [M]mgen addr
  196. Display options: show [m]mgen addr, r[e]draw screen
  197. Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view,
  198. add [l]abel, [D]elete address, [R]efresh balance:
  199. """
  200. key_mappings = {
  201. 'a':'s_amt','d':'s_addr','r':'d_reverse','M':'s_twmmid',
  202. 'm':'d_mmid','e':'d_redraw',
  203. 'q':'a_quit','p':'a_print','v':'a_view','w':'a_view_wide',
  204. 'l':'a_lbl_add','D':'a_addr_delete','R':'a_balance_refresh' }
  205. async def __init__(self,proto,*args,**kwargs):
  206. if g.cached_balances:
  207. self.hdr_fmt += '\n' + yellow('WARNING: Using cached balances. These may be out of date!')
  208. await TwUnspentOutputs.__init__(self,proto,*args,**kwargs)
  209. def do_sort(self,key=None,reverse=False):
  210. if key == 'txid': return
  211. super().do_sort(key=key,reverse=reverse)
  212. async def get_unspent_rpc(self):
  213. wl = self.wallet.sorted_list
  214. if self.addrs:
  215. wl = [d for d in wl if d['addr'] in self.addrs]
  216. return [{
  217. 'account': TwLabel(self.proto,d['mmid']+' '+d['comment']),
  218. 'address': d['addr'],
  219. 'amount': await self.wallet.get_balance(d['addr']),
  220. 'confirmations': 0, # TODO
  221. } for d in wl]
  222. class MMGenTwUnspentOutput(TwUnspentOutputs.MMGenTwUnspentOutput):
  223. valid_attrs = {'txid','vout','amt','amt2','label','twmmid','addr','confs','skip'}
  224. invalid_attrs = {'proto'}
  225. def age_disp(self,o,age_fmt): # TODO
  226. return None
  227. class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
  228. disp_type = 'token'
  229. prompt_fs = 'Total to spend: {} {}\n\n'
  230. col_adj = 37
  231. def get_display_precision(self):
  232. return 10 # truncate precision for narrow display
  233. async def get_unspent_data(self,*args,**kwargs):
  234. await super().get_unspent_data(*args,**kwargs)
  235. for e in self.unspent:
  236. e.amt2 = await self.wallet.get_eth_balance(e.addr)
  237. class EthereumTwAddrList(TwAddrList):
  238. has_age = False
  239. async def __init__(self,proto,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None):
  240. self.proto = proto
  241. self.wallet = wallet or await TrackingWallet(self.proto,mode='w')
  242. tw_dict = self.wallet.mmid_ordered_dict
  243. self.total = self.proto.coin_amt('0')
  244. from mmgen.obj import CoinAddr
  245. for mmid,d in list(tw_dict.items()):
  246. # if d['confirmations'] < minconf: continue # cannot get confirmations for eth account
  247. label = TwLabel(self.proto,mmid+' '+d['comment'])
  248. if usr_addr_list and (label.mmid not in usr_addr_list):
  249. continue
  250. bal = await self.wallet.get_balance(d['addr'])
  251. if bal == 0 and not showempty:
  252. if not label.comment or not all_labels:
  253. continue
  254. self[label.mmid] = {'amt': self.proto.coin_amt('0'), 'lbl': label }
  255. if showbtcaddrs:
  256. self[label.mmid]['addr'] = CoinAddr(self.proto,d['addr'])
  257. self[label.mmid]['lbl'].mmid.confs = None
  258. self[label.mmid]['amt'] += bal
  259. self.total += bal
  260. del self.wallet
  261. class EthereumTokenTwAddrList(EthereumTwAddrList):
  262. pass
  263. class EthereumTwGetBalance(TwGetBalance):
  264. fs = '{w:13} {c}\n' # TODO - for now, just suppress display of meaningless data
  265. async def __init__(self,proto,*args,**kwargs):
  266. self.wallet = await TrackingWallet(proto,mode='w')
  267. await TwGetBalance.__init__(self,proto,*args,**kwargs)
  268. async def create_data(self):
  269. data = self.wallet.mmid_ordered_dict
  270. for d in data:
  271. if d.type == 'mmgen':
  272. key = d.obj.sid
  273. if key not in self.data:
  274. self.data[key] = [self.proto.coin_amt('0')] * 4
  275. else:
  276. key = 'Non-MMGen'
  277. conf_level = 2 # TODO
  278. amt = await self.wallet.get_balance(data[d]['addr'])
  279. self.data['TOTAL'][conf_level] += amt
  280. self.data[key][conf_level] += amt
  281. del self.wallet
  282. class EthereumTwAddrData(TwAddrData):
  283. async def get_tw_data(self,wallet=None):
  284. vmsg('Getting address data from tracking wallet')
  285. tw = (wallet or await TrackingWallet(self.proto)).mmid_ordered_dict
  286. # emulate the output of RPC 'listaccounts' and 'getaddressesbyaccount'
  287. return [(mmid+' '+d['comment'],[d['addr']]) for mmid,d in list(tw.items())]
  288. class EthereumTokenTwGetBalance(EthereumTwGetBalance): pass
  289. class EthereumTokenTwAddrData(EthereumTwAddrData): pass
  290. class EthereumAddrData(AddrData): pass
  291. class EthereumTokenAddrData(EthereumAddrData): pass