new.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2026 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.new: Ethereum new transaction class
  12. """
  13. from ....tx import new as TxBase
  14. from ....obj import Int, ETHNonce
  15. from ....util import msg, ymsg, is_int
  16. from ...vm.tx.new import New as VmNew
  17. from ..contract import Token
  18. from .base import Base, TokenBase
  19. class New(VmNew, Base, TxBase.New):
  20. fee_fail_fs = 'Network fee estimation failed'
  21. usr_fee_prompt = 'Enter transaction fee or gas price: '
  22. byte_cost = 68 # https://ethereum.stackexchange.com/questions/39401/
  23. # how-do-you-calculate-gas-limit-for-transaction-with-data-in-ethereum
  24. def __init__(self, *args, **kwargs):
  25. super().__init__(*args, **kwargs)
  26. if self.cfg.contract_data:
  27. m = "'--contract-data' option may not be used with token transaction"
  28. assert 'Token' not in self.name, m
  29. with open(self.cfg.contract_data) as fp:
  30. self.usr_contract_data = bytes.fromhex(fp.read().strip())
  31. self.disable_fee_check = True
  32. async def get_gas_estimateGas(self, *, to_addr):
  33. return self.dfl_gas
  34. async def set_gas(self, *, to_addr=None, force=False):
  35. if force or to_addr or not hasattr(self, 'gas'):
  36. if is_int(self.cfg.gas):
  37. self.gas = int(self.cfg.gas)
  38. elif self.cfg.gas == 'fallback':
  39. self.gas = self.dfl_gas
  40. elif self.is_bump and not self.rpc.daemon.id == 'reth':
  41. self.gas = self.txobj['startGas']
  42. else:
  43. assert self.cfg.gas in ('auto', None), f'{self.cfg.gas}: invalid value for cfg.gas'
  44. self.gas = await self.get_gas_estimateGas(to_addr=to_addr)
  45. async def get_nonce(self):
  46. return ETHNonce(int(
  47. await self.rpc.call('eth_getTransactionCount', '0x'+self.inputs[0].addr, 'pending'), 16))
  48. async def make_txobj(self): # called by create_serialized()
  49. self.txobj = {
  50. 'from': self.inputs[0].addr,
  51. 'to': self.outputs[0].addr if self.outputs else None,
  52. 'amt': self.outputs[0].amt if self.outputs else self.proto.coin_amt('0'),
  53. 'gasPrice': self.fee_abs2gasprice(self.usr_fee),
  54. 'startGas': self.gas,
  55. 'nonce': await self.get_nonce(),
  56. 'chainId': self.rpc.chainID,
  57. 'data': self.usr_contract_data.hex()}
  58. def set_gas_with_data(self, data):
  59. if not self.is_token:
  60. self.gas = self.dfl_gas + self.byte_cost * len(data)
  61. # one-shot method
  62. def adj_gas_with_extra_data_len(self, extra_data_len):
  63. if not (self.is_token or hasattr(self, '_gas_adjusted')):
  64. self.gas += self.byte_cost * extra_data_len
  65. self._gas_adjusted = True
  66. @property
  67. def network_estimated_fee_label(self):
  68. return 'Network-estimated'
  69. def network_fee_to_unit_disp(self, net_fee):
  70. return '{} Gwei'.format(self.pretty_fmt_fee(
  71. self.proto.coin_amt(net_fee.fee, from_unit='wei').to_unit('Gwei')))
  72. # get rel_fee (gas price) from network, return in native wei
  73. async def get_rel_fee_from_network(self):
  74. return self._net_fee(
  75. Int(await self.rpc.call('eth_gasPrice'), base=16),
  76. 'eth_gasPrice')
  77. # given rel fee and units, return absolute fee using self.total_gas
  78. def fee_rel2abs(self, tx_size, amt_in_units, unit):
  79. return self.proto.coin_amt(int(amt_in_units * self.total_gas), from_unit=unit)
  80. # given fee estimate (gas price) in wei, return absolute fee, adjusting by self.cfg.fee_adjust
  81. def fee_est2abs(self, net_fee):
  82. ret = self.fee_gasPrice2abs(net_fee.fee) * self.cfg.fee_adjust
  83. if self.cfg.verbose:
  84. msg(f'Estimated fee: {net_fee.fee} ETH')
  85. return ret
  86. def convert_and_check_fee(self, fee, desc):
  87. abs_fee = self.feespec2abs(fee, None)
  88. if abs_fee is False:
  89. return False
  90. elif not self.disable_fee_check and (abs_fee > self.proto.max_tx_fee):
  91. msg('{} {c}: {} fee too large (maximum fee: {} {c})'.format(
  92. abs_fee.hl(),
  93. desc,
  94. self.proto.max_tx_fee.hl(),
  95. c = self.proto.coin))
  96. return False
  97. else:
  98. return abs_fee
  99. class TokenNew(TokenBase, New):
  100. desc = 'transaction'
  101. fee_is_approximate = True
  102. async def set_gas(self, *, to_addr=None, force=False):
  103. await super().set_gas(to_addr=to_addr, force=force)
  104. if self.is_swap and (force or not hasattr(self, 'router_gas')):
  105. self.router_gas = (
  106. int(self.cfg.router_gas) if self.cfg.router_gas else
  107. self.txobj['router_gas'] if self.txobj else
  108. self.dfl_router_gas)
  109. @property
  110. def total_gas(self):
  111. return self.gas + (self.router_gas if self.is_swap else 0)
  112. async def get_gas_estimateGas(self, *, to_addr=None):
  113. t = Token(
  114. self.cfg,
  115. self.proto,
  116. self.twctl.token,
  117. decimals = self.twctl.decimals,
  118. rpc = self.rpc)
  119. data = t.create_transfer_data(
  120. to_addr = to_addr or self.outputs[0].addr,
  121. amt = self.outputs[0].amt or await self.twctl.get_balance(self.inputs[0].addr),
  122. op = self.token_op)
  123. try:
  124. res = await t.do_call(
  125. f'{self.token_op}(address,uint256)',
  126. method = 'eth_estimateGas',
  127. from_addr = self.inputs[0].addr,
  128. data = data)
  129. except Exception as e:
  130. ymsg(
  131. 'Unable to estimate gas limit via node. '
  132. 'Please retry with --gas set to an integer value, or ‘fallback’ for a sane default')
  133. raise e
  134. return int(res, 16)
  135. async def make_txobj(self): # called by create_serialized()
  136. await super().make_txobj()
  137. t = Token(self.cfg, self.proto, self.twctl.token, decimals=self.twctl.decimals)
  138. o = self.txobj
  139. o['token_addr'] = t.addr
  140. o['decimals'] = t.decimals
  141. o['token_to'] = o['to']
  142. if self.is_swap:
  143. o['expiry'] = self.quote_data.data['expiry']
  144. o['router_gas'] = self.router_gas
  145. def update_change_output(self, funds_left):
  146. if self.outputs[0].is_chg:
  147. self.update_output_amt(0, self.inputs[0].amt)
  148. # token transaction, so check both eth and token balances
  149. # TODO: add test with insufficient funds
  150. async def precheck_sufficient_funds(self, inputs_sum, sel_unspent, outputs_sum):
  151. eth_bal = await self.twctl.get_eth_balance(sel_unspent[0].addr)
  152. if eth_bal == 0: # we don't know the fee yet
  153. msg('This account has no ether to pay for the transaction fee!')
  154. return False
  155. return await super().precheck_sufficient_funds(inputs_sum, sel_unspent, outputs_sum)
  156. async def get_funds_available(self, fee, outputs_sum):
  157. bal = await self.twctl.get_eth_balance(self.inputs[0].addr)
  158. return self._funds_available(bal >= fee, bal - fee if bal >= fee else fee - bal)
  159. def final_inputs_ok_msg(self, funds_left):
  160. token_bal = (
  161. self.proto.coin_amt('0') if self.outputs[0].is_chg
  162. else self.inputs[0].amt - self.outputs[0].amt
  163. )
  164. return "Transaction leaves ≈{} {} and {} {} in the sender's account".format(
  165. funds_left.hl(),
  166. self.proto.coin,
  167. token_bal.hl(),
  168. self.proto.dcoin
  169. )