contract.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2024 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.contract: Ethereum ERC20 token classes
  20. """
  21. from decimal import Decimal
  22. from . import rlp
  23. from . import erigon_sleep
  24. from ...util import msg,pp_msg,die
  25. from ...base_obj import AsyncInit
  26. from ...obj import MMGenObject,CoinTxID
  27. from ...addr import CoinAddr,TokenAddr
  28. def parse_abi(s):
  29. return [s[:8]] + [s[8+x*64:8+(x+1)*64] for x in range(len(s[8:])//64)]
  30. class TokenCommon(MMGenObject):
  31. def create_method_id(self,sig):
  32. return self.keccak_256(sig.encode()).hexdigest()[:8]
  33. def transferdata2sendaddr(self,data): # online
  34. return CoinAddr(self.proto,parse_abi(data)[1][-40:])
  35. def transferdata2amt(self,data): # online
  36. return self.proto.coin_amt(int(parse_abi(data)[-1],16) * self.base_unit)
  37. async def do_call(self,method_sig,method_args='',toUnit=False):
  38. data = self.create_method_id(method_sig) + method_args
  39. if self.cfg.debug:
  40. msg('ETH_CALL {}: {}'.format(
  41. method_sig,
  42. '\n '.join(parse_abi(data)) ))
  43. ret = await self.rpc.call('eth_call',{ 'to': '0x'+self.addr, 'data': '0x'+data },'pending')
  44. await erigon_sleep(self)
  45. if toUnit:
  46. return int(ret,16) * self.base_unit
  47. else:
  48. return ret
  49. async def get_balance(self,acct_addr):
  50. return self.proto.coin_amt(await self.do_call('balanceOf(address)',acct_addr.rjust(64,'0'),toUnit=True))
  51. def strip(self,s):
  52. return ''.join([chr(b) for b in s if 32 <= b <= 127]).strip()
  53. async def get_name(self):
  54. return self.strip(bytes.fromhex((await self.do_call('name()'))[2:]))
  55. async def get_symbol(self):
  56. return self.strip(bytes.fromhex((await self.do_call('symbol()'))[2:]))
  57. async def get_decimals(self):
  58. ret = await self.do_call('decimals()')
  59. try:
  60. assert ret[:2] == '0x'
  61. return int(ret,16)
  62. except:
  63. msg(f'RPC call to decimals() failed (returned {ret!r})')
  64. return None
  65. async def get_total_supply(self):
  66. return await self.do_call('totalSupply()',toUnit=True)
  67. async def info(self):
  68. return ('{:15}{}\n' * 5).format(
  69. 'token address:', self.addr,
  70. 'token symbol:', await self.get_symbol(),
  71. 'token name:', await self.get_name(),
  72. 'decimals:', self.decimals,
  73. 'total supply:', await self.get_total_supply() )
  74. async def code(self):
  75. return (await self.rpc.call('eth_getCode','0x'+self.addr))[2:]
  76. def create_data(
  77. self,
  78. to_addr,
  79. amt,
  80. method_sig = 'transfer(address,uint256)'):
  81. from_arg = ''
  82. to_arg = to_addr.rjust(64,'0')
  83. amt_arg = '{:064x}'.format( int(amt / self.base_unit) )
  84. return self.create_method_id(method_sig) + from_arg + to_arg + amt_arg
  85. def make_tx_in(
  86. self,
  87. to_addr,
  88. amt,
  89. start_gas,
  90. gasPrice,
  91. nonce,
  92. method_sig = 'transfer(address,uint256)'):
  93. data = self.create_data(
  94. to_addr,
  95. amt,
  96. method_sig = method_sig)
  97. return {
  98. 'to': bytes.fromhex(self.addr),
  99. 'startgas': start_gas.toWei(),
  100. 'gasprice': gasPrice.toWei(),
  101. 'value': 0,
  102. 'nonce': nonce,
  103. 'data': bytes.fromhex(data)}
  104. async def txsign(self,tx_in,key,from_addr,chain_id=None):
  105. from .pyethereum.transactions import Transaction
  106. if chain_id is None:
  107. res = await self.rpc.call('eth_chainId')
  108. chain_id = None if res is None else int(res,16)
  109. tx = Transaction(**tx_in).sign(key,chain_id)
  110. if tx.sender.hex() != from_addr:
  111. die(3,f'Sender address {from_addr!r} does not match address of key {tx.sender.hex()!r}!')
  112. if self.cfg.debug:
  113. msg('TOKEN DATA:')
  114. pp_msg(tx.to_dict())
  115. msg('PARSED ABI DATA:\n {}'.format(
  116. '\n '.join(parse_abi(tx.data.hex())) ))
  117. return (
  118. rlp.encode(tx).hex(),
  119. CoinTxID(tx.hash.hex())
  120. )
  121. # The following are used for token deployment only:
  122. async def txsend(self,txhex):
  123. return (await self.rpc.call('eth_sendRawTransaction','0x'+txhex)).replace('0x','',1)
  124. async def transfer(
  125. self,
  126. from_addr,
  127. to_addr,
  128. amt,
  129. key,
  130. start_gas,
  131. gasPrice,
  132. method_sig = 'transfer(address,uint256)'):
  133. tx_in = self.make_tx_in(
  134. to_addr,
  135. amt,
  136. start_gas,
  137. gasPrice,
  138. nonce = int(await self.rpc.call('eth_getTransactionCount','0x'+from_addr,'pending'),16),
  139. method_sig = method_sig)
  140. txhex,_ = await self.txsign(tx_in,key,from_addr)
  141. return await self.txsend(txhex)
  142. class Token(TokenCommon):
  143. def __init__(self,cfg,proto,addr,decimals,rpc=None):
  144. if type(self).__name__ == 'Token':
  145. from ...util2 import get_keccak
  146. self.keccak_256 = get_keccak(cfg)
  147. self.cfg = cfg
  148. self.proto = proto
  149. self.addr = TokenAddr(proto,addr)
  150. assert isinstance(decimals,int),f'decimals param must be int instance, not {type(decimals)}'
  151. self.decimals = decimals
  152. self.base_unit = Decimal('10') ** -self.decimals
  153. self.rpc = rpc
  154. class ResolvedToken(TokenCommon,metaclass=AsyncInit):
  155. async def __init__(self,cfg,proto,rpc,addr):
  156. from ...util2 import get_keccak
  157. self.keccak_256 = get_keccak(cfg)
  158. self.cfg = cfg
  159. self.proto = proto
  160. self.rpc = rpc
  161. self.addr = TokenAddr(proto,addr)
  162. decimals = await self.get_decimals() # requires self.addr!
  163. if not decimals:
  164. die( 'TokenNotInBlockchain', f'Token {addr!r} not in blockchain' )
  165. Token.__init__(self,cfg,proto,addr,decimals,rpc)