Browse Source

proto.eth.tx: use `eth_estimateGas` for token transactions

The MMGen Project 7 months ago
parent
commit
82294e6a88

+ 1 - 1
mmgen/data/release_date

@@ -1 +1 @@
-April 2025
+May 2025

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev33
+15.1.dev34

+ 15 - 0
mmgen/help/help_notes.py

@@ -118,6 +118,21 @@ FMT CODES:
 		from ..tx import BaseTX
 		return BaseTX(cfg=self.cfg, proto=self.proto).rel_fee_desc
 
+	def gas_limit(self, target):
+		return """
+                                 GAS LIMIT
+
+This option specifies the maximum gas allowance for an Ethereum transaction.
+It’s generally of interest only for token transactions or swap transactions
+from token assets.
+
+Parameter must be an integer or one of the special values ‘fallback’ (for a
+locally computed sane default) or ‘auto’ (for gas estimate via an RPC call,
+in the case of a token transaction, or locally computed default, in the case
+of a standard transaction). The default is ‘auto’.
+
+		""" if target == 'swaptx' or self.proto.base_coin == 'ETH' else ''
+
 	def fee(self, all_coins=False):
 		from ..tx import BaseTX
 		text = """

+ 7 - 5
mmgen/main_txcreate.py

@@ -57,10 +57,11 @@ opts_data = {
 			+                        {fu} (an integer followed by {fl}).
 			+                        See FEE SPECIFICATION below.  If omitted, fee will be
 			+                        calculated using network fee estimation.
-			et -g, --gas=N           Specify gas limit (integer)
-			-s -g, --gas=N           Specify gas limit for Ethereum (integer)
-			-s -G, --router-gas=N    Specify gas limit for Ethereum router contract
-			+                        (integer). Applicable only for swaps from token assets
+			et -g, --gas=N           Set the gas limit (see GAS LIMIT below)
+			-s -g, --gas=N           Set the gas limit for Ethereum (see GAS LIMIT below)
+			-s -G, --router-gas=N    Set the gas limit for the Ethereum router contract
+			+                        (integer).  When unset, a hardcoded default will be
+			+                        used.  Applicable only for swaps from token assets.
 			-- -i, --info            Display {a_info} and exit
 			-- -I, --inputs=      i  Specify transaction inputs (comma-separated list of
 			+                        MMGen IDs or coin addresses).  Note that ALL unspent
@@ -83,7 +84,7 @@ opts_data = {
 			-- -y, --yes             Answer 'yes' to prompts, suppress non-essential output
 			e- -X, --cached-balances Use cached balances
 		""",
-		'notes': '\n{c}\n{n_at}\n\n{F}\n\n{x}',
+		'notes': '\n{c}\n{n_at}\n\n{g}{F}\n\n{x}',
 	},
 	'code': {
 		'usage': lambda cfg, proto, help_notes, s: s.format(
@@ -100,6 +101,7 @@ opts_data = {
 			x_dfl = cfg._autoset_opts['swap_proto'].choices[0]),
 		'notes': lambda cfg, help_mod, help_notes, s: s.format(
 			c      = help_mod(f'{target}create'),
+			g      = help_notes('gas_limit', target),
 			F      = help_notes('fee', all_coins={'tx': False, 'swaptx': True}[target]),
 			n_at   = help_notes('address_types'),
 			x      = help_mod(f'{target}create_examples'))

+ 7 - 5
mmgen/main_txdo.py

@@ -57,10 +57,11 @@ opts_data = {
 			+                         {fu} (an integer followed by {fl!r}).
 			+                         See FEE SPECIFICATION below.  If omitted, fee will be
 			+                         calculated using network fee estimation.
-			et -g, --gas=N            Specify gas limit (integer)
-			-s -g, --gas=N            Specify gas limit for Ethereum (integer)
-			-s -G, --router-gas=N     Specify gas limit for Ethereum router contract
-			+                         (integer). Applicable only for swaps from token assets
+			et -g, --gas=N            Set the gas limit (see GAS LIMIT below)
+			-s -g, --gas=N            Set the gas limit for Ethereum (see GAS LIMIT below)
+			-s -G, --router-gas=N     Set the gas limit for the Ethereum router contract
+			+                         (integer).  When unset, a hardcoded default will be
+			+                         used.  Applicable only for swaps from token assets.
 			-- -H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
 			+                        'f' at offset 'o' (comma-separated)
 			-- -i, --in-fmt=        f Input is from wallet format 'f' (see FMT CODES below)
@@ -110,7 +111,7 @@ opts_data = {
 {c}
 {n_at}
 
-{F}
+{g}{F}
 
 
                                  SIGNING NOTES
@@ -145,6 +146,7 @@ column below:
 			x_dfl   = cfg._autoset_opts['swap_proto'].choices[0]),
 		'notes': lambda cfg, help_mod, help_notes, s: s.format(
 			c       = help_mod(f'{target}create'),
+			g       = help_notes('gas_limit', target),
 			F       = help_notes('fee'),
 			n_at    = help_notes('address_types'),
 			f       = help_notes('fmt_codes'),

+ 3 - 0
mmgen/proto/btc/tx/new.py

@@ -24,6 +24,9 @@ class New(Base, TxNew):
 	no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
 	msg_insufficient_funds = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
 
+	async def set_gas(self, *, to_addr=None):
+		return None
+
 	def process_data_output_arg(self, arg):
 		if any(arg.startswith(pfx) for pfx in ('data:', 'hexdata:')):
 			if hasattr(self, '_have_op_return_data'):

+ 9 - 3
mmgen/proto/eth/contract.py

@@ -55,24 +55,30 @@ class Contract:
 
 	async def do_call(
 			self,
-			method_sig,
+			method_sig  = '',
 			method_args = '',
 			*,
+			method      = 'eth_call',
 			block       = 'pending', # earliest, latest, safe, finalized
+			from_addr   = None,
+			data        = None,
 			toUnit      = False):
 
-		data = self.create_method_id(method_sig) + method_args
+		data = data or (self.create_method_id(method_sig) + method_args)
 
 		args = {
 			'to': '0x' + self.addr,
 			'input': '0x' + data}
 
+		if from_addr:
+			args['from'] = '0x' + from_addr
+
 		if self.cfg.debug:
 			msg('ETH_CALL {}:  {}'.format(
 				method_sig,
 				'\n  '.join(parse_abi(data))))
 
-		ret = await self.rpc.call('eth_call', args, block)
+		ret = await self.rpc.call(method, args, block)
 
 		await erigon_sleep(self)
 

+ 32 - 3
mmgen/proto/eth/tx/new.py

@@ -16,7 +16,7 @@ import json
 
 from ....tx import new as TxBase
 from ....obj import Int, ETHNonce, MMGenTxID
-from ....util import msg, is_int, is_hex_str, make_chksum_6, suf, die
+from ....util import msg, ymsg, is_int, is_hex_str, make_chksum_6, suf, die
 from ....tw.ctl import TwCtl
 from ....addr import is_mmgen_id, is_coin_addr
 from ..contract import Token
@@ -35,8 +35,6 @@ class New(Base, TxBase.New):
 
 		super().__init__(*args, **kwargs)
 
-		self.gas = int(self.cfg.gas or self.dfl_gas)
-
 		if self.is_token and self.is_swap:
 			self.router_gas = int(self.cfg.router_gas or self.dfl_router_gas)
 
@@ -47,6 +45,14 @@ class New(Base, TxBase.New):
 				self.usr_contract_data = bytes.fromhex(fp.read().strip())
 			self.disable_fee_check = True
 
+	async def set_gas(self, *, to_addr=None):
+		if to_addr or not hasattr(self, 'gas'):
+			auto_gas = self.cfg.gas in ('auto', None)
+			self.gas = (
+				self.dfl_gas if self.cfg.gas == 'fallback' or (auto_gas and not self.is_token) else
+				(await self.get_gas_estimateGas(to_addr=to_addr)) if auto_gas else
+				int(self.cfg.gas))
+
 	async def get_nonce(self):
 		return ETHNonce(int(
 			await self.rpc.call('eth_getTransactionCount', '0x'+self.inputs[0].addr, 'pending'), 16))
@@ -218,6 +224,29 @@ class TokenNew(TokenBase, New):
 	def total_gas(self):
 		return self.gas + (self.router_gas if self.is_swap else 0)
 
+	async def get_gas_estimateGas(self, *, to_addr=None):
+		t = Token(
+			self.cfg,
+			self.proto,
+			self.twctl.token,
+			decimals = self.twctl.decimals,
+			rpc = self.rpc)
+
+		data = t.create_transfer_data(
+			to_addr = to_addr or self.outputs[0].addr,
+			amt = self.outputs[0].amt or await self.twuo.twctl.get_balance(self.inputs[0].addr),
+			op = self.token_op)
+
+		try:
+			res = await t.do_call(method='eth_estimateGas', from_addr=self.inputs[0].addr, data=data)
+		except Exception as e:
+			ymsg(
+				'Unable to estimate gas limit via node. '
+				'Please retry with --gas set to an integer value, or ‘fallback’ for a sane default')
+			raise e
+
+		return int(res, 16)
+
 	async def make_txobj(self): # called by create_serialized()
 		await super().make_txobj()
 		t = Token(self.cfg, self.proto, self.twctl.token, decimals=self.twctl.decimals)

+ 2 - 0
mmgen/tx/bump.py

@@ -72,6 +72,8 @@ class Bump(Completed, NewSwap):
 
 		output_idx = self.choose_output()
 
+		await self.set_gas()
+
 		if not silent:
 			msg('Minimum fee for new transaction: {} {} ({} {})'.format(
 				self.min_fee.hl(),

+ 3 - 1
mmgen/tx/new.py

@@ -493,11 +493,13 @@ class New(Base):
 		while True:
 			if not await self.get_inputs(outputs_sum):
 				continue
-			fee_hint = None
 			if self.is_swap:
 				fee_hint = await self.update_vault_output(
 					self.vault_output.amt or self.sum_inputs(),
 					deduct_est_fee = self.vault_output == self.chg_output)
+			else:
+				await self.set_gas()
+				fee_hint = None
 			desc = 'User-selected' if self.cfg.fee else 'Recommended' if fee_hint else None
 			if (funds_left := await self.get_fee(
 					self.cfg.fee or fee_hint,

+ 1 - 0
mmgen/tx/new_swap.py

@@ -199,6 +199,7 @@ class NewSwap(New):
 		while True:
 			self.cfg._util.qmsg(f'Retrieving data from {c.rpc.host}...')
 			c.get_quote()
+			await self.set_gas(to_addr=c.router if self.is_token else None)
 			self.swap_quote_refresh_time = time.time()
 			trade_limit = get_trade_limit()
 			self.cfg._util.qmsg('OK')

+ 1 - 1
test/cmdtest_d/ethdev.py

@@ -1597,7 +1597,7 @@ class CmdTestEthdev(CmdTestEthdevMethods, CmdTestBase, CmdTestShared):
 	def token_txdo_cached_balances(self):
 		return self.txdo_cached_balances(
 			acct          = '1',
-			fee_info_data = ('0.00375', '50'),
+			fee_info_data = ('0.00260265', '50'),
 			add_args      = ['--token=mm1', '98831F3A:E:12,43.21'])
 
 	def token_txcreate_refresh_balances(self):

+ 7 - 7
test/cmdtest_d/ethswap.py

@@ -419,13 +419,13 @@ class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev
 			expect = ':2019e4/3/0')
 
 	def swaptxcreate3a(self):
-		t = self._swaptxcreate(['ETH', '0.7654321', 'ETH.MM1'])
+		t = self._swaptxcreate(['ETH', '0.7654321', 'ETH.MM1'], add_opts=['--gas=fallback'])
 		t.expect(f'{dfl_sid}:E:4') # check that correct unused address was found
 		t.expect('(Y/n): ', 'y')
 		return self._swaptxcreate_ui_common(t)
 
 	def swaptxcreate3b(self):
-		t = self._swaptxcreate(['ETH', '8.765', 'ETH.MM1', f'{dfl_sid}:E:5'])
+		t = self._swaptxcreate(['ETH', '8.765', 'ETH.MM1', f'{dfl_sid}:E:5'], add_opts=['--gas=auto'])
 		return self._swaptxcreate_ui_common(t)
 
 	async def swaptxmemo3(self):
@@ -441,19 +441,19 @@ class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev
 		return 'ok'
 
 	def swaptxcreate4(self):
-		t = self._swaptxcreate(['ETH.MM1', '87.654321', 'BTC', f'{dfl_sid}:C:2'])
+		t = self._swaptxcreate(['ETH.MM1', '87.654321', 'BTC', f'{dfl_sid}:C:2'], add_opts=['--gas=auto'])
 		return self._swaptxcreate_ui_common(t)
 
 	def swaptxcreate5a(self):
-		t = self._swaptxcreate(['ETH.MM1', '98.7654321', 'ETH'])
+		t = self._swaptxcreate(
+			['ETH.MM1', '98.7654321', 'ETH'],
+			add_opts = ['--gas=58000', '--router-gas=500000'])
 		t.expect(f'{dfl_sid}:E:13') # check that correct unused address was found
 		t.expect('(Y/n): ', 'y')
 		return self._swaptxcreate_ui_common(t)
 
 	def swaptxcreate5b(self):
-		t = self._swaptxcreate(
-			['ETH.MM1', '98.7654321', 'ETH', f'{dfl_sid}:E:12'],
-			add_opts = ['--gas=58000', '--router-gas=500000'])
+		t = self._swaptxcreate(['ETH.MM1', '98.7654321', 'ETH', f'{dfl_sid}:E:12'])
 		return self._swaptxcreate_ui_common(t)
 
 	def swaptxsign1(self):