tw.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. #!/usr/bin/env python3
  2. # -*- coding: UTF-8 -*-
  3. #
  4. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  5. # Copyright (C)2013-2018 The MMGen Project <mmgen@tuta.io>
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. """
  20. altcoins.eth.tw: Ethereum tracking wallet and related classes for the MMGen suite
  21. """
  22. import json
  23. from mmgen.common import *
  24. from mmgen.obj import ETHAmt,TwMMGenID,TwComment,TwLabel
  25. from mmgen.tw import TrackingWallet,TwAddrList,TwUnspentOutputs
  26. from mmgen.addr import AddrData
  27. from mmgen.altcoins.eth.contract import Token
  28. class EthereumTrackingWallet(TrackingWallet):
  29. desc = 'Ethereum tracking wallet'
  30. caps = ()
  31. data_dir = os.path.join(g.altcoin_data_dir,g.coin.lower(),g.proto.data_subdir)
  32. tw_file = os.path.join(data_dir,'tracking-wallet.json')
  33. def __init__(self,mode='r'):
  34. TrackingWallet.__init__(self,mode=mode)
  35. check_or_create_dir(self.data_dir)
  36. try:
  37. self.orig_data = get_data_from_file(self.tw_file,silent=True)
  38. self.data = json.loads(self.orig_data)
  39. except:
  40. try: os.stat(self.tw_file)
  41. except:
  42. self.orig_data = ''
  43. self.data = {'coin':g.coin,'accounts':{},'tokens':{}}
  44. else: die(2,"File '{}' exists but does not contain valid json data")
  45. else:
  46. self.upgrade_wallet_maybe()
  47. m = 'Tracking wallet coin ({}) does not match current coin ({})!'
  48. assert self.data['coin'] == g.coin,m.format(self.data['coin'],g.coin)
  49. if not 'tokens' in self.data:
  50. self.data['tokens'] = {}
  51. def conv_types(ad):
  52. for v in ad.values():
  53. v['mmid'] = TwMMGenID(v['mmid'],on_fail='raise')
  54. v['comment'] = TwComment(v['comment'],on_fail='raise')
  55. conv_types(self.data['accounts'])
  56. for v in self.data['tokens'].values():
  57. conv_types(v)
  58. def upgrade_wallet_maybe(self):
  59. if not 'accounts' in self.data or not 'coin' in self.data:
  60. ymsg('Upgrading {}!'.format(self.desc))
  61. if not 'accounts' in self.data:
  62. self.data = {}
  63. self.data['accounts'] = json.loads(self.orig_data)
  64. if not 'coin' in self.data:
  65. self.data['coin'] = g.coin
  66. mode_save = self.mode
  67. self.mode = 'w'
  68. self.write()
  69. self.mode = mode_save
  70. self.orig_data = json.dumps(self.data)
  71. msg('{} upgraded successfully!'.format(self.desc))
  72. def data_root(self): return self.data['accounts']
  73. def data_root_desc(self): return 'accounts'
  74. @write_mode
  75. def import_address(self,addr,label,foo):
  76. ad = self.data_root()
  77. if addr in ad:
  78. if not ad[addr]['mmid'] and label.mmid:
  79. msg("Warning: MMGen ID '{}' was missing in tracking wallet!".format(label.mmid))
  80. elif ad[addr]['mmid'] != label.mmid:
  81. die(3,"MMGen ID '{}' does not match tracking wallet!".format(label.mmid))
  82. ad[addr] = { 'mmid': label.mmid, 'comment': label.comment }
  83. @write_mode
  84. def write(self): # use 'check_data' to check wallet hasn't been altered by another program
  85. write_data_to_file( self.tw_file,
  86. json.dumps(self.data),'Ethereum tracking wallet data',
  87. ask_overwrite=False,ignore_opt_outdir=True,silent=True,
  88. check_data=True,cmp_data=self.orig_data)
  89. @write_mode
  90. def delete_all(self):
  91. self.data = {}
  92. self.write()
  93. @write_mode
  94. def remove_address(self,addr):
  95. root = self.data_root()
  96. from mmgen.obj import is_coin_addr,is_mmgen_id
  97. if is_coin_addr(addr):
  98. have_match = lambda k: k == addr
  99. elif is_mmgen_id(addr):
  100. have_match = lambda k: root[k]['mmid'] == addr
  101. else:
  102. die(1,"'{}' is not an Ethereum address or MMGen ID".format(addr))
  103. for k in root:
  104. if have_match(k):
  105. # return the addr resolved to mmid if possible
  106. ret = root[k]['mmid'] if is_mmgen_id(root[k]['mmid']) else addr
  107. del root[k]
  108. self.write()
  109. return ret
  110. else:
  111. m = "Address '{}' not found in '{}' section of tracking wallet"
  112. msg(m.format(addr,self.data_root_desc()))
  113. return None
  114. def is_in_wallet(self,addr):
  115. return addr in self.data_root()
  116. def sorted_list(self):
  117. return sorted(
  118. map(lambda x: {'addr':x[0],'mmid':x[1]['mmid'],'comment':x[1]['comment']},self.data_root().items()),
  119. key=lambda x: x['mmid'].sort_key+x['addr'] )
  120. def mmid_ordered_dict(self):
  121. from collections import OrderedDict
  122. return OrderedDict(map(lambda x: (x['mmid'],{'addr':x['addr'],'comment':x['comment']}), self.sorted_list()))
  123. @write_mode
  124. def set_label(self,coinaddr,lbl):
  125. for addr,d in self.data_root().items():
  126. if addr == coinaddr:
  127. d['comment'] = lbl.comment
  128. self.write()
  129. return None
  130. else: # emulate on_fail='return' of RPC library
  131. m = "Address '{}' not found in '{}' section of tracking wallet"
  132. return ('rpcfail',(None,2,m.format(coinaddr,self.data_root_desc())))
  133. class EthereumTokenTrackingWallet(EthereumTrackingWallet):
  134. def token_is_in_wallet(self,addr):
  135. return addr in self.data['tokens']
  136. def data_root_desc(self):
  137. return 'token ' + Token(g.token).symbol()
  138. @write_mode
  139. def add_token(self,token):
  140. msg("Adding token '{}' to tracking wallet.".format(token))
  141. self.data['tokens'][token] = {}
  142. def data_root(self): # create the token data root if necessary
  143. if g.token not in self.data['tokens']:
  144. self.add_token(g.token)
  145. return self.data['tokens'][g.token]
  146. def sym2addr(self,sym): # online
  147. for addr in self.data['tokens']:
  148. if Token(addr).symbol().upper() == sym.upper():
  149. return addr
  150. return None
  151. # No unspent outputs with Ethereum, but naming must be consistent
  152. class EthereumTwUnspentOutputs(TwUnspentOutputs):
  153. disp_type = 'eth'
  154. can_group = False
  155. col_adj = 29
  156. hdr_fmt = 'TRACKED ACCOUNTS (sort order: {})\nTotal {}: {}'
  157. desc = 'account balances'
  158. item_desc = 'account'
  159. dump_fn_pfx = 'balances'
  160. prompt = """
  161. Sort options: [a]mount, a[d]dress, [r]everse, [M]mgen addr
  162. Display options: show [m]mgen addr, r[e]draw screen
  163. Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view,
  164. add [l]abel, [R]emove address:
  165. """
  166. key_mappings = {
  167. 'a':'s_amt','d':'s_addr','r':'d_reverse','M':'s_twmmid',
  168. 'm':'d_mmid','e':'d_redraw',
  169. 'q':'a_quit','p':'a_print','v':'a_view','w':'a_view_wide',
  170. 'l':'a_lbl_add','R':'a_addr_remove' }
  171. def do_sort(self,key=None,reverse=False):
  172. if key == 'txid': return
  173. super(EthereumTwUnspentOutputs,self).do_sort(key=key,reverse=reverse)
  174. def get_addr_bal(self,addr):
  175. return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
  176. def get_unspent_rpc(self):
  177. rpc_init()
  178. return map(lambda d: {
  179. 'account': TwLabel(d['mmid']+' '+d['comment'],on_fail='raise'),
  180. 'address': d['addr'],
  181. 'amount': self.get_addr_bal(d['addr']),
  182. 'confirmations': 0, # TODO
  183. }, TrackingWallet().sorted_list())
  184. class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
  185. disp_type = 'token'
  186. prompt_fs = 'Total to spend: {} {}\n\n'
  187. col_adj = 37
  188. def get_display_precision(self): return 10 # truncate precision for narrow display
  189. def get_addr_bal(self,addr):
  190. return Token(g.token).balance(addr)
  191. def get_unspent_data(self):
  192. super(type(self),self).get_unspent_data()
  193. for e in self.unspent:
  194. e.amt2 = ETHAmt(int(g.rpch.eth_getBalance('0x'+e.addr),16),'wei')
  195. class EthereumTwAddrList(TwAddrList):
  196. def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels):
  197. rpc_init()
  198. if g.token: self.token = Token(g.token)
  199. tw = TrackingWallet().mmid_ordered_dict()
  200. self.total = g.proto.coin_amt('0')
  201. from mmgen.obj import CoinAddr
  202. for mmid,d in tw.items():
  203. # if d['confirmations'] < minconf: continue # cannot get confirmations for eth account
  204. label = TwLabel(mmid+' '+d['comment'],on_fail='raise')
  205. if usr_addr_list and (label.mmid not in usr_addr_list): continue
  206. bal = self.get_addr_balance(d['addr'])
  207. if bal == 0 and not showempty:
  208. if not label.comment: continue
  209. if not all_labels: continue
  210. self[label.mmid] = {'amt': g.proto.coin_amt('0'), 'lbl': label }
  211. if showbtcaddrs:
  212. self[label.mmid]['addr'] = CoinAddr(d['addr'])
  213. self[label.mmid]['lbl'].mmid.confs = None
  214. self[label.mmid]['amt'] += bal
  215. self.total += bal
  216. def get_addr_balance(self,addr):
  217. return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
  218. class EthereumTokenTwAddrList(EthereumTwAddrList):
  219. def get_addr_balance(self,addr):
  220. return self.token.balance(addr)
  221. from mmgen.tw import TwGetBalance
  222. class EthereumTwGetBalance(TwGetBalance):
  223. fs = '{w:13} {c}\n' # TODO - for now, just suppress display of meaningless data
  224. def create_data(self):
  225. data = TrackingWallet().mmid_ordered_dict()
  226. for d in data:
  227. if d.type == 'mmgen':
  228. key = d.obj.sid
  229. if key not in self.data:
  230. self.data[key] = [g.proto.coin_amt('0')] * 4
  231. else:
  232. key = 'Non-MMGen'
  233. conf_level = 2 # TODO
  234. amt = self.get_addr_balance(data[d]['addr'])
  235. self.data['TOTAL'][conf_level] += amt
  236. self.data[key][conf_level] += amt
  237. def get_addr_balance(self,addr):
  238. return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
  239. class EthereumTokenTwGetBalance(EthereumTwGetBalance):
  240. def get_addr_balance(self,addr):
  241. return Token(g.token).balance(addr)
  242. class EthereumAddrData(AddrData):
  243. @classmethod
  244. def get_tw_data(cls):
  245. vmsg('Getting address data from tracking wallet')
  246. tw = TrackingWallet().mmid_ordered_dict()
  247. # emulate the output of RPC 'listaccounts' and 'getaddressesbyaccount'
  248. return [(mmid+' '+d['comment'],[d['addr']]) for mmid,d in tw.items()]
  249. class EthereumTokenAddrData(EthereumAddrData): pass