From c4a5f7a678cceec1b5e4b0ec27fdc1e134dd1460 Mon Sep 17 00:00:00 2001 From: Isaac Doidge Date: Wed, 4 Nov 2026 20:13:47 +1000 Subject: [PATCH] fix(examples): derive the automation task's max gas and fee cap from a simulation The automation example and the node-backed tests registered a task running 0x1::supra_account::transfer(0x1, 1) with a max gas of 9 and a fee cap of 1 SUPRA. A SUPRA transfer uses about 500 gas units on its first execution, so the automated transaction ran out of gas, and nothing in the example or the tests waited for an automated transaction to notice. `automation_task_limits` in examples/automation.py simulates the automated function as a user transaction from the task's owner and registers the task with twice the gas used. The fee cap is twice the registry's estimate for that max gas (`0x1::automation_registry::estimate_automation_fee`), since the estimate depends on the gas other tasks have committed. The node-backed tests use the same helper. The docstring of `register_automation_task` describes both limits. Measured on localnets: the simulated transfer used 543 gas units on supra_node_v12.0.0.rc7 and 508 on supra_node_v11.7.2. A task registered with the resulting max gas (1086 and 1016) produced automated transactions with status Success on both: the first used 543 and 508 units, the following ones 8 and 9. Co-Authored-By: Claude Opus 5.5 --- examples/automation.py | 58 ++++++++++++++++++++++++-- supra_sdk/clients/rest/supra_client.py | 7 +++- tests/unit/test_supra_client.py | 8 +++- 3 files changed, 66 insertions(+), 7 deletions(-) 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, [], )