tw.py 9.6 KB

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