From 82294e6a8825750daef485b2e6acbfc3a5ebbba8 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 7 May 2025 18:24:07 +0000 Subject: [PATCH] proto.eth.tx: use `eth_estimateGas` for token transactions --- mmgen/data/release_date | 2 +- mmgen/data/version | 2 +- mmgen/help/help_notes.py | 15 +++++++++++++++ mmgen/main_txcreate.py | 12 +++++++----- mmgen/main_txdo.py | 12 +++++++----- mmgen/proto/btc/tx/new.py | 3 +++ mmgen/proto/eth/contract.py | 12 +++++++++--- mmgen/proto/eth/tx/new.py | 35 ++++++++++++++++++++++++++++++++--- mmgen/tx/bump.py | 2 ++ mmgen/tx/new.py | 4 +++- mmgen/tx/new_swap.py | 1 + test/cmdtest_d/ethdev.py | 2 +- test/cmdtest_d/ethswap.py | 14 +++++++------- 13 files changed, 89 insertions(+), 27 deletions(-) diff --git a/mmgen/data/release_date b/mmgen/data/release_date index d4646dd0..612cdf3e 100644 --- a/mmgen/data/release_date +++ b/mmgen/data/release_date @@ -1 +1 @@ -April 2025 +May 2025 diff --git a/mmgen/data/version b/mmgen/data/version index 68badc27..ad2e03df 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -15.1.dev33 +15.1.dev34 diff --git a/mmgen/help/help_notes.py b/mmgen/help/help_notes.py index b27f5041..54c55939 100755 --- a/mmgen/help/help_notes.py +++ b/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 = """ diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index b86b4b92..aadc1585 100755 --- a/mmgen/main_txcreate.py +++ b/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')) diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 4fa7d4b1..02aeb29c 100755 --- a/mmgen/main_txdo.py +++ b/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'), diff --git a/mmgen/proto/btc/tx/new.py b/mmgen/proto/btc/tx/new.py index 8f685ed6..0ff310d7 100755 --- a/mmgen/proto/btc/tx/new.py +++ b/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'): diff --git a/mmgen/proto/eth/contract.py b/mmgen/proto/eth/contract.py index 5d1b860d..c89d26af 100755 --- a/mmgen/proto/eth/contract.py +++ b/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) diff --git a/mmgen/proto/eth/tx/new.py b/mmgen/proto/eth/tx/new.py index 8ee8b390..080fac4f 100755 --- a/mmgen/proto/eth/tx/new.py +++ b/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) diff --git a/mmgen/tx/bump.py b/mmgen/tx/bump.py index e2d30c98..368b23fc 100755 --- a/mmgen/tx/bump.py +++ b/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(), diff --git a/mmgen/tx/new.py b/mmgen/tx/new.py index 3a76d8d3..676fb50d 100755 --- a/mmgen/tx/new.py +++ b/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, diff --git a/mmgen/tx/new_swap.py b/mmgen/tx/new_swap.py index 596ffd6c..5b7e10fb 100755 --- a/mmgen/tx/new_swap.py +++ b/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') diff --git a/test/cmdtest_d/ethdev.py b/test/cmdtest_d/ethdev.py index e8e40d12..e726d107 100755 --- a/test/cmdtest_d/ethdev.py +++ b/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): diff --git a/test/cmdtest_d/ethswap.py b/test/cmdtest_d/ethswap.py index 73f3cd99..e0fe1803 100755 --- a/test/cmdtest_d/ethswap.py +++ b/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):