proto.eth.tx: use eth_estimateGas for token transactions

This commit is contained in:
The MMGen Project 2025-05-07 18:24:07 +00:00
commit 82294e6a88
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
13 changed files with 89 additions and 27 deletions

View file

@ -1 +1 @@
April 2025
May 2025

View file

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

View file

@ -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.
Its 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 = """

View file

@ -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'))

View file

@ -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'),

View file

@ -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'):

View file

@ -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)

View file

@ -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)

View file

@ -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(),

View file

@ -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,

View file

@ -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')

View file

@ -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):

View file

@ -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):