diff --git a/examples/automation.py b/examples/automation.py index 82fabaa..8801d3c 100644 --- a/examples/automation.py +++ b/examples/automation.py @@ -13,7 +13,11 @@ from supra_sdk.clients.rest import SupraClient from supra_sdk.clients.rest.supra_client import TransactionFailedError from supra_sdk.clients.rest.transaction_defaults import MaxGasEstimationError -from supra_sdk.transactions import EntryFunction, TransactionArgument +from supra_sdk.transactions import ( + EntryFunction, + TransactionArgument, + TransactionPayload, +) TASK_DURATION_SECONDS = 3600 @@ -37,6 +41,51 @@ async def outside_cycle_transition(action: Callable[[], Awaitable[str]]) -> str: raise AssertionError("unreachable") +# Max gas of the simulated transaction. The node checks that the sender's balance covers +# it at the gas price, so it stays below what a faucet-funded account holds; raise it for +# a function that uses more. +SIMULATION_MAX_GAS = 5_000 +# The automated transaction runs in a later block, on state that may differ from the +# simulated one, so the task's max gas is this many times the simulated gas. +MAX_GAS_FACTOR = 2 +# The registry's fee estimate rises with the gas other tasks have committed, which can +# change before the registration is included, so the fee cap is this many times the +# estimate. +FEE_CAP_FACTOR = 2 + + +async def automation_task_limits( + supra_client: SupraClient, sender: Account, automated_function: EntryFunction +) -> tuple[int, int]: + """Returns the max gas and the fee cap for the epoch to register a task with. + + The max gas is `MAX_GAS_FACTOR` times the gas that `automated_function` uses when + `sender` sends it as a user transaction. The fee cap is `FEE_CAP_FACTOR` times + the registry's estimate of the epoch fee for that max gas + (`0x1::automation_registry::estimate_automation_fee`). The registry refuses a + registration whose cap is below its estimate, and charges the cap at registration. + """ + simulation = await supra_client.simulate_transaction( + await supra_client.create_signed_transaction( + sender, + TransactionPayload(automated_function), + max_gas_amount=SIMULATION_MAX_GAS, + ) + ) + move_output = simulation["output"]["Move"] + if simulation["status"] != "Success": + raise RuntimeError(f"The automated function does not succeed: {move_output}") + max_gas = MAX_GAS_FACTOR * int(move_output["gas_used"]) + estimated_fee = int( + ( + await supra_client.view( + "0x1::automation_registry::estimate_automation_fee", [], [str(max_gas)] + ) + )[0] + ) + return max_gas, FEE_CAP_FACTOR * estimated_fee + + async def main() -> None: supra_client = SupraClient(RPC_NODE_URL) sender = Account.generate() @@ -54,6 +103,9 @@ async def main() -> None: TransactionArgument(1, Serializer.u64), ], ) + max_gas, fee_cap_for_epoch = await automation_task_limits( + supra_client, sender, automated_function + ) # Without a task type the task is registered with version 1 of the registration # parameters. `automation_task_type` selects version 2. `automation_priority` is # accepted only with `AutomationTaskType.SYSTEM`, from an account the registry has @@ -76,9 +128,9 @@ async def main() -> None: lambda: supra_client.register_automation_task( sender, automated_function, - 9, + max_gas, gas_price_cap, - 1 * (10**8), + fee_cap_for_epoch, expiry_time, [], ) diff --git a/supra_sdk/clients/rest/supra_client.py b/supra_sdk/clients/rest/supra_client.py index 2f2d3fd..4da82aa 100644 --- a/supra_sdk/clients/rest/supra_client.py +++ b/supra_sdk/clients/rest/supra_client.py @@ -882,9 +882,12 @@ async def register_automation_task( owner_account (Account): Account registering an automation task. It will be eligible to cancel or stop task. automated_function (str): Automated entry function payload. - automation_max_gas_amount (int): Max gas allowed for automation. + automation_max_gas_amount (int): Max gas allowed for automation. An automated transaction that needs + more gas than this fails, and its owner pays for the gas it used. Simulate the automated function + as a user transaction and use its gas with a margin (see `examples/automation.py`). automation_gas_price_cap (int): Gas price cap for automation execution. - automation_fee_cap_for_epoch (int): Maximum total fee for the epoch. + automation_fee_cap_for_epoch (int): Maximum total fee for the epoch. The registry refuses a cap below + its estimate for the max gas (the view `0x1::automation_registry::estimate_automation_fee`). automation_expiration_timestamp_secs (int): Expiration time for automation. automation_aux_data (list[bytes]): Auxiliary data for automation. automation_task_type (AutomationTaskType | None): ``USER`` or ``SYSTEM``. diff --git a/tests/unit/test_supra_client.py b/tests/unit/test_supra_client.py index 685ffab..41c1dca 100644 --- a/tests/unit/test_supra_client.py +++ b/tests/unit/test_supra_client.py @@ -6,6 +6,7 @@ import unittest from typing import Any, cast +from examples.automation import automation_task_limits from supra_sdk.account import Account from supra_sdk.account_address import AccountAddress from supra_sdk.bcs import Serializer @@ -510,14 +511,17 @@ async def register_test_automation_task(self) -> str: TransactionArgument(1, Serializer.u64), ], ) + max_gas, fee_cap_for_epoch = await automation_task_limits( + self.supra_client, self.sender, automated_function + ) # The node refuses a cap below its minimum gas price. These tests do not wait for # the task to execute, so the minimum is enough. tx_hash = await self.supra_client.register_automation_task( self.sender, automated_function, - 9, + max_gas, (await self.supra_client.estimate_gas_price())["min_configured_gas_price"], - 1 * (10**8), + fee_cap_for_epoch, int(time.time()) + 7200, [], )