tw.py 9.0 KB

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