base.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. proto.eth.tx.base: Ethereum base transaction class
  12. """
  13. from collections import namedtuple
  14. from ....tx.base import Base as TxBase
  15. from ....obj import Int
  16. class Base(TxBase):
  17. rel_fee_desc = 'gas price'
  18. rel_fee_disp = 'gas price in Gwei'
  19. txobj = None
  20. dfl_gas = 21000 # the startGas amt used in the transaction
  21. # for simple sends with no data, startGas = 21000
  22. contract_desc = 'contract'
  23. usr_contract_data = b''
  24. disable_fee_check = False
  25. @property
  26. def nondata_outputs(self):
  27. return self.outputs
  28. def pretty_fmt_fee(self, fee):
  29. if fee < 10:
  30. return f'{fee:.3f}'.rstrip('0').rstrip('.')
  31. return str(int(fee))
  32. # given absolute fee in ETH, return gas price for display in selected unit
  33. def fee_abs2rel(self, abs_fee, *, to_unit='Gwei'):
  34. return self.pretty_fmt_fee(
  35. self.fee_abs2gasprice(abs_fee).to_unit(to_unit))
  36. # given absolute fee in ETH, return gas price in ETH
  37. def fee_abs2gasprice(self, abs_fee):
  38. return self.proto.coin_amt(int(abs_fee.toWei() // self.gas.toWei()), from_unit='wei')
  39. # given rel fee (gasPrice) in wei, return absolute fee using self.gas (Ethereum-only method)
  40. def fee_gasPrice2abs(self, rel_fee):
  41. assert isinstance(rel_fee, int), f'{rel_fee!r}: incorrect type for fee estimate (not an integer)'
  42. return self.proto.coin_amt(rel_fee * self.gas.toWei(), from_unit='wei')
  43. def is_replaceable(self):
  44. return True
  45. async def get_receipt(self, txid, *, receipt_only=False):
  46. import asyncio
  47. from ....util import msg, msg_r
  48. for n in range(60):
  49. rx = await self.rpc.call('eth_getTransactionReceipt', '0x'+txid) # -> null if pending
  50. if rx or not self.cfg.wait:
  51. break
  52. if n == 0:
  53. msg_r('Waiting for first confirmation..')
  54. await asyncio.sleep(1)
  55. msg_r('.')
  56. if rx:
  57. if n:
  58. msg('OK')
  59. if receipt_only:
  60. return rx
  61. else:
  62. if self.cfg.wait:
  63. msg('timeout exceeded!')
  64. return None
  65. tx = await self.rpc.call('eth_getTransactionByHash', '0x'+txid)
  66. return namedtuple('exec_status',
  67. ['status', 'gas_sent', 'gas_used', 'gas_price', 'contract_addr', 'tx', 'rx'])(
  68. status = Int(rx['status'], base=16), # zero is failure, non-zero success
  69. gas_sent = Int(tx['gas'], base=16),
  70. gas_used = Int(rx['gasUsed'], base=16),
  71. gas_price = self.proto.coin_amt(Int(tx['gasPrice'], base=16), from_unit='wei'),
  72. contract_addr = self.proto.coin_addr(rx['contractAddress'][2:])
  73. if rx['contractAddress'] else None,
  74. tx = tx,
  75. rx = rx)
  76. def check_serialized_integrity(self):
  77. if self.signed:
  78. from .. import rlp
  79. o = self.txobj
  80. d = rlp.decode(bytes.fromhex(self.serialized))
  81. to_key = 'token_addr' if self.is_token else 'to'
  82. if o['nonce'] == 0:
  83. assert d[0] == b'', f'{d[0]}: invalid nonce in serialized data'
  84. else:
  85. assert int(d[0].hex(), 16) == o['nonce'], f'{d[0]}: invalid nonce in serialized data'
  86. if o.get(to_key):
  87. assert d[3].hex() == o[to_key], f'{d[3].hex()}: invalid ‘to’ address in serialized data'
  88. if not self.is_token:
  89. if o['amt']:
  90. assert int(d[4].hex(), 16) == o['amt'].toWei(), (
  91. f'{d[4].hex()}: invalid amt in serialized data')
  92. if self.is_swap:
  93. assert d[5] == self.swap_memo.encode(), (
  94. f'{d[5]}: invalid swap memo in serialized data')
  95. class TokenBase(Base):
  96. dfl_gas = 75000
  97. contract_desc = 'token contract'
  98. def check_serialized_integrity(self):
  99. if self.signed:
  100. super().check_serialized_integrity()
  101. from .. import rlp
  102. from ....amt import TokenAmt
  103. d = rlp.decode(bytes.fromhex(self.serialized))
  104. o = self.txobj
  105. assert d[4] == b'', f'{d[4]}: non-empty amount field in token transaction in serialized data'
  106. data = d[5].hex()
  107. assert data[:8] == ('095ea7b3' if self.is_swap else 'a9059cbb'), (
  108. f'{data[:8]}: invalid MethodID for op ‘{self.token_op}’ in serialized data')
  109. assert data[32:72] == o['token_to'], (
  110. f'{data[32:72]}: invalid ‘token_to‘ address in serialized data')
  111. assert TokenAmt(
  112. int(data[72:], 16),
  113. decimals = o['decimals'],
  114. from_unit = 'atomic') == o['amt'], (
  115. f'{data[72:]}: invalid amt in serialized data')
  116. if self.is_swap:
  117. d = rlp.decode(bytes.fromhex(self.serialized2))
  118. data = d[5].hex()
  119. assert data[:8] == '44bc937b', (
  120. f'{data[:8]}: invalid MethodID in router TX serialized data')
  121. assert data[32:72] == self.token_vault_addr, (
  122. f'{data[32:72]}: invalid vault address in router TX serialized data')
  123. memo = bytes.fromhex(data[392:])[:len(self.swap_memo)]
  124. assert memo == self.swap_memo.encode(), (
  125. f'{memo}: invalid swap memo in router TX serialized data')
  126. assert TokenAmt(
  127. int(data[136:200], 16),
  128. decimals = o['decimals'],
  129. from_unit = 'atomic') == o['amt'], (
  130. f'{data[136:200]}: invalid amt in router TX serialized data')