ctl.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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. proto.eth.tw.ctl: Ethereum tracking wallet control class
  20. """
  21. from ....util import msg,ymsg,die
  22. from ....tw.ctl import TwCtl,write_mode
  23. from ....addr import is_coin_addr,is_mmgen_id
  24. from ..contract import Token,TokenResolve
  25. class EthereumTwCtl(TwCtl):
  26. caps = ('batch',)
  27. data_key = 'accounts'
  28. use_tw_file = True
  29. def init_empty(self):
  30. self.data = {
  31. 'coin': self.proto.coin,
  32. 'network': self.proto.network.upper(),
  33. 'accounts': {},
  34. 'tokens': {},
  35. }
  36. def upgrade_wallet_maybe(self):
  37. upgraded = False
  38. if not 'accounts' in self.data or not 'coin' in self.data:
  39. ymsg(f'Upgrading {self.desc} (v1->v2: accounts field added)')
  40. if not 'accounts' in self.data:
  41. self.data = {}
  42. import json
  43. self.data['accounts'] = json.loads(self.orig_data)
  44. if not 'coin' in self.data:
  45. self.data['coin'] = self.proto.coin
  46. upgraded = True
  47. def have_token_params_fields():
  48. for k in self.data['tokens']:
  49. if 'params' in self.data['tokens'][k]:
  50. return True
  51. def add_token_params_fields():
  52. for k in self.data['tokens']:
  53. self.data['tokens'][k]['params'] = {}
  54. if not 'tokens' in self.data:
  55. self.data['tokens'] = {}
  56. upgraded = True
  57. if self.data['tokens'] and not have_token_params_fields():
  58. ymsg(f'Upgrading {self.desc} (v2->v3: token params fields added)')
  59. add_token_params_fields()
  60. upgraded = True
  61. if not 'network' in self.data:
  62. ymsg(f'Upgrading {self.desc} (v3->v4: network field added)')
  63. self.data['network'] = self.proto.network.upper()
  64. upgraded = True
  65. if upgraded:
  66. self.force_write()
  67. msg(f'{self.desc} upgraded successfully!')
  68. async def rpc_get_balance(self,addr):
  69. return self.proto.coin_amt(int(await self.rpc.call('eth_getBalance','0x'+addr,'latest'),16),'wei')
  70. @write_mode
  71. async def batch_import_address(self,args_list):
  72. return [await self.import_address(*a) for a in args_list]
  73. async def rescan_addresses(self,coin_addrs):
  74. pass
  75. @write_mode
  76. async def import_address(self,addr,label):
  77. r = self.data_root
  78. if addr in r:
  79. if not r[addr]['mmid'] and label.mmid:
  80. msg(f'Warning: MMGen ID {label.mmid!r} was missing in tracking wallet!')
  81. elif r[addr]['mmid'] != label.mmid:
  82. die(3,'MMGen ID {label.mmid!r} does not match tracking wallet!')
  83. r[addr] = { 'mmid': label.mmid, 'comment': label.comment }
  84. @write_mode
  85. async def remove_address(self,addr):
  86. r = self.data_root
  87. if is_coin_addr(self.proto,addr):
  88. have_match = lambda k: k == addr
  89. elif is_mmgen_id(self.proto,addr):
  90. have_match = lambda k: r[k]['mmid'] == addr
  91. else:
  92. die(1,f'{addr!r} is not an Ethereum address or MMGen ID')
  93. for k in r:
  94. if have_match(k):
  95. # return the addr resolved to mmid if possible
  96. ret = r[k]['mmid'] if is_mmgen_id(self.proto,r[k]['mmid']) else addr
  97. del r[k]
  98. self.write()
  99. return ret
  100. else:
  101. msg(f'Address {addr!r} not found in {self.data_root_desc!r} section of tracking wallet')
  102. return None
  103. @write_mode
  104. async def set_label(self,coinaddr,lbl):
  105. for addr,d in list(self.data_root.items()):
  106. if addr == coinaddr:
  107. d['comment'] = lbl.comment
  108. self.write()
  109. return True
  110. else:
  111. msg(f'Address {coinaddr!r} not found in {self.data_root_desc!r} section of tracking wallet')
  112. return False
  113. async def addr2sym(self,req_addr):
  114. for addr in self.data['tokens']:
  115. if addr == req_addr:
  116. return self.data['tokens'][addr]['params']['symbol']
  117. else:
  118. return None
  119. async def sym2addr(self,sym):
  120. for addr in self.data['tokens']:
  121. if self.data['tokens'][addr]['params']['symbol'] == sym.upper():
  122. return addr
  123. else:
  124. return None
  125. def get_token_param(self,token,param):
  126. if token in self.data['tokens']:
  127. return self.data['tokens'][token]['params'].get(param)
  128. return None
  129. @property
  130. def sorted_list(self):
  131. return sorted(
  132. [ { 'addr':x[0],
  133. 'mmid':x[1]['mmid'],
  134. 'comment':x[1]['comment'] }
  135. for x in self.data_root.items() if x[0] not in ('params','coin') ],
  136. key=lambda x: x['mmid'].sort_key+x['addr'] )
  137. @property
  138. def mmid_ordered_dict(self):
  139. return dict((x['mmid'],{'addr':x['addr'],'comment':x['comment']}) for x in self.sorted_list)
  140. class EthereumTokenTwCtl(EthereumTwCtl):
  141. desc = 'Ethereum token tracking wallet'
  142. decimals = None
  143. symbol = None
  144. cur_eth_balances = {}
  145. async def __init__(self,proto,mode='r',token_addr=None):
  146. await super().__init__(proto,mode=mode)
  147. for v in self.data['tokens'].values():
  148. self.conv_types(v)
  149. if self.importing and token_addr:
  150. if not is_coin_addr(proto,token_addr):
  151. die( 'InvalidTokenAddress', f'{token_addr!r}: invalid token address' )
  152. else:
  153. assert token_addr == None,'EthereumTokenTwCtl_chk1'
  154. token_addr = await self.sym2addr(proto.tokensym) # returns None on failure
  155. if not is_coin_addr(proto,token_addr):
  156. die( 'UnrecognizedTokenSymbol', f'Specified token {proto.tokensym!r} could not be resolved!' )
  157. from ....addr import TokenAddr
  158. self.token = TokenAddr(proto,token_addr)
  159. if self.token not in self.data['tokens']:
  160. if self.importing:
  161. await self.import_token(self.token)
  162. else:
  163. die( 'TokenNotInWallet', f'Specified token {self.token!r} not in wallet!' )
  164. self.decimals = self.get_param('decimals')
  165. self.symbol = self.get_param('symbol')
  166. proto.tokensym = self.symbol
  167. @property
  168. def data_root(self):
  169. return self.data['tokens'][self.token]
  170. @property
  171. def data_root_desc(self):
  172. return 'token ' + self.get_param('symbol')
  173. async def rpc_get_balance(self,addr):
  174. return await Token(self.proto,self.token,self.decimals,self.rpc).get_balance(addr)
  175. async def get_eth_balance(self,addr,force_rpc=False):
  176. cache = self.cur_eth_balances
  177. r = self.data['accounts']
  178. ret = None if force_rpc else self.get_cached_balance(addr,cache,r)
  179. if ret == None:
  180. ret = await super().rpc_get_balance(addr)
  181. self.cache_balance(addr,ret,cache,r)
  182. return ret
  183. def get_param(self,param):
  184. return self.data['tokens'][self.token]['params'][param]
  185. @write_mode
  186. async def import_token(self,tokenaddr):
  187. """
  188. Token 'symbol' and 'decimals' values are resolved from the network by the system just
  189. once, upon token import. Thereafter, token address, symbol and decimals are resolved
  190. either from the tracking wallet (online operations) or transaction file (when signing).
  191. """
  192. t = await TokenResolve(self.proto,self.rpc,tokenaddr)
  193. self.data['tokens'][tokenaddr] = {
  194. 'params': {
  195. 'symbol': await t.get_symbol(),
  196. 'decimals': t.decimals
  197. }
  198. }