Skip to content
This repository has been archived by the owner on Dec 1, 2023. It is now read-only.

Auto calculate max_fee #332

Merged
merged 31 commits into from
Jan 9, 2023

Conversation

andrew-fleming
Copy link
Contributor

Resolves #284.

This PR proposes to automatically estimate the max_fee when the max_fee isn't explicitly passed (which would be interpreted as 0). The fee estimation logic lies in estimate_fee_when_zero.

Some notes:

CLI

  • estimate_fee_when_zero precedes tx.execute in the run_transaction function.

NRE

  • The NRE includes a new method execute_tx which estimates the fee (if zero) and executes the transaction. If accepted, the proposed changes will then offer users two ways to script account txs:
    await nre.execute_tx(
        account.send(ETH_TOKEN_ADDRESS, "transfer", [recipient, *amount])
    )

    OR:

    tx = await account.send(ETH_TOKEN_ADDRESS, "transfer", [recipient, *amount])
    max_fee = await tx.estimate_fee()
    await tx.update_fee(max_fee).execute()

Copy link
Member

@ericnordelo ericnordelo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good Andrew! Glad to see no big changes were required from 320. Left some comments.

src/nile/common.py Outdated Show resolved Hide resolved
src/nile/common.py Outdated Show resolved Hide resolved
src/nile/nre.py Outdated
@@ -33,6 +33,12 @@ def call(self, address_or_alias, method, params=None, abi=None):
address_or_alias, "call", method, params, self.network, abi=abi
)

async def execute_tx(self, tx_wrapper, watch_mode=None):
"""Add estimated max_fee (if max_fee is zero) and execute transaction."""
tx = await tx_wrapper
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is better to keep the convention of receiving objects and not promises (for the sake of the API simplicity).

I understand that the idea is to allow:

await nre.execute_tx(
        account.send(ETH_TOKEN_ADDRESS, "transfer", [recipient, *amount])
    )

instead of

await nre.execute_tx(
        await account.send(ETH_TOKEN_ADDRESS, "transfer", [recipient, *amount])
    )

But I think it will be confusing for a user trying to achieve something like this:

tx = await account.send(ETH_TOKEN_ADDRESS, "transfer", [recipient, *amount])
await nre.execute_tx(tx)

And the last looks cleaner IMO. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that sounds good to me. I was trying just trying to simplify as much as possible, I agree that your proposed solution looks cleaner

src/nile/nre.py Outdated
@@ -33,6 +33,12 @@ def call(self, address_or_alias, method, params=None, abi=None):
address_or_alias, "call", method, params, self.network, abi=abi
)

async def execute_tx(self, tx_wrapper, watch_mode=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping in mind that this method is modifying the tx (not only executing it), I would rename it to something more descriptive. Not sure what name to use though, maybe set_estimated_fee_and_execute? This name doesn't take into account that if the fee is different than 0 the estimated one won't be used, so I think is still confusing.

Keeping in mind that execute_tx updates tx.hash, tx.max_fee, and tx.query_hash, users can get unexpected behavior on the script by calling this execute_tx method without knowing that it modifies the tx object.

I'm more in favor of keeping only this way, which is more explicit and easier to follow because of that:

tx = await account.send(ETH_TOKEN_ADDRESS, "transfer", [recipient, *amount])
max_fee = await tx.estimate_fee()
await tx.update_fee(max_fee).execute()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping in mind that this method is modifying the tx (not only executing it), I would rename it to something more descriptive

Agreed. The idea was to have a simple and easy-to-use method. It's probably better to have set_estimated_fee_and_execute if we keep this approach

users can get unexpected behavior on the script by calling this execute_tx method without knowing that it modifies the tx object

I'm more in favor of keeping only this way, which is more explicit and easier to follow because of that:

My only contention with this consists of the "auto calculate fee" issue itself because we would be asking the user to set up the update_fee and it wouldn't exactly be automated in the NRE (though, the flow makes it quite easy). I suppose the question is: do you think the tx.update_fee flow is enough to satisfy the issue?

Copy link
Member

@ericnordelo ericnordelo Jan 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got you. I think having the current design, in NRE update_fee is enough to satisfy because issues by misleading tx hashes after execution weigh more than the benefit over the two extra lines IMO (in CLI happens transparently).

Maybe we could implement an "auto fee" mechanism in Transaction initialization that would initialize the transaction with the estimated fee, transparently to the user (if the max_fee in construction time is set to -1 or "auto" in initialization for example), what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a better idea! And it's implemented, I just need to update tests

"""Execute the transaction."""
if self.max_fee == 0:
max_fee = await self.estimate_fee(signer)
self.update_fee(max_fee)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rationale for me not adding the max_fee directly to the execute method was the fact that the transaction was going to be updated (tx.hash, and tx.max_fee) without the user noting this.

The same thing happens if we add this logic here. When I commented about the "auto" option, I was thinking of something in construction time, not in execution. I still think changing the tx.hash in execute without the user noting is not the best design.

In construction time, at least the tx.hash and the max_fee will be set to the estimated fee from the beginning. So something like this won't be possible:

# Bad design
tx = await account.send(max_fee=0...)

print(f"Sending: {tx.hash}") 

# The printed hash is not the correct one, because this would change it
await tx.execute()

Copy link
Member

@ericnordelo ericnordelo Jan 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think that we should let the user set the max_fee as 0, that's why I think "auto" would be a better option for auto fee estimation. Then we know the user won't receive a different transaction hash from the one he was expecting (it could be one with max_fee equal 0, even if it doesn't make much sense to us, it could make sense).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah forgive me, I lost sight of the tx.hash changing. The issue I was running into with that approach is estimate_fee is async, so I think executing this in during construction (with the sync __post_init__) won't work without getting into hacky territory. Because of the async issue, I don't think we can pass auto or -1 to the Transaction during construction either.

Two options that I see are:

  1. Have the users use tx.update_fee which IMO isn't automating the fee but as you mentioned, maybe it's enough
  2. Automate the fee in the account methods which keeps the tx.hash consistent for the user

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Number 2 sounds great. Returning the tx_wrapper with the estimated fee if max_fee is left to None in account methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On it!

Comment on lines 273 to 275
if max_fee is None:
estimated_fee = await tx.estimate_fee()
tx.update_fee(estimated_fee)
Copy link
Member

@ericnordelo ericnordelo Jan 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with this we are good to go, just a small suggestion for organization.

Keeping in mind we are calling this for all the account methods returning tx wrappers. Can't we add this logic in the BaseTxWrapper __post_init__ method to avoid having to call the _set_estimated_fee_if_none method repeatedly in account?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was the idea early on, but we'd need __post_init__ to be async in order to call the underlying tx.estimate_fee. It should be possible to do, but I think it's a bit hacky (a la the Account initialization with the async self.deploy)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, makes sense.

Copy link
Member

@ericnordelo ericnordelo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there! I left a couple of comments to keep the Transaction code as clean as possible, and I think we are good to go.

Copy link
Member

@ericnordelo ericnordelo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there :)!

src/nile/core/types/account.py Outdated Show resolved Hide resolved
Co-authored-by: Eric Nordelo <eric.nordelo39@gmail.com>
Copy link
Member

@ericnordelo ericnordelo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@ericnordelo ericnordelo merged commit 08fb7be into OpenZeppelin:main Jan 9, 2023
@andrew-fleming andrew-fleming deleted the add-auto-max_fee branch January 10, 2023 23:28
):
# mock_tx_wrapper = AsyncMock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be here?

Copy link
Contributor Author

@andrew-fleming andrew-fleming Jan 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not!

#345

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

auto calculate max_fee
3 participants