From 85dcf519466f1cc6ea2de92641104a2c8b694515 Mon Sep 17 00:00:00 2001 From: 1yam Date: Mon, 31 Mar 2025 12:37:08 +0000 Subject: [PATCH 01/12] fix: use estimate_gas in can_transact for accurate limit checks --- src/aleph/sdk/chains/ethereum.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/aleph/sdk/chains/ethereum.py b/src/aleph/sdk/chains/ethereum.py index 8815825e..2585fdcd 100644 --- a/src/aleph/sdk/chains/ethereum.py +++ b/src/aleph/sdk/chains/ethereum.py @@ -119,13 +119,20 @@ def connect_chain(self, chain: Optional[Chain] = None): def switch_chain(self, chain: Optional[Chain] = None): self.connect_chain(chain=chain) - def can_transact(self, block=True) -> bool: + def can_transact(self, tx: TxParams, block=True) -> bool: balance = self.get_eth_balance() - valid = balance > MIN_ETH_BALANCE_WEI if self.chain else False + try: + estimated_gas = self._provider.eth.estimate_gas(tx) + except Exception: + estimated_gas = ( + MIN_ETH_BALANCE # Fallback to MIN_ETH_BALANCE if estimation fails + ) + + valid = balance > estimated_gas if self.chain else False if not valid and block: raise InsufficientFundsError( token_type=TokenType.GAS, - required_funds=MIN_ETH_BALANCE, + required_funds=estimated_gas, available_funds=float(from_wei_token(balance)), ) return valid @@ -136,7 +143,6 @@ async def _sign_and_send_transaction(self, tx_params: TxParams) -> str: @param tx_params - Transaction parameters @returns - str - Transaction hash """ - self.can_transact() def sign_and_send() -> TxReceipt: if self._provider is None: @@ -144,6 +150,7 @@ def sign_and_send() -> TxReceipt: signed_tx = self._provider.eth.account.sign_transaction( tx_params, self._account.key ) + tx_hash = self._provider.eth.send_raw_transaction(signed_tx.raw_transaction) tx_receipt = self._provider.eth.wait_for_transaction_receipt( tx_hash, settings.TX_TIMEOUT From f804f99a8ba729e45330bd7d00e10613bde3278d Mon Sep 17 00:00:00 2001 From: 1yam Date: Mon, 31 Mar 2025 12:41:35 +0000 Subject: [PATCH 02/12] feat: implement _simulate_create_tx_flow to estimate gas for specific Superfluid transactions --- src/aleph/sdk/connectors/superfluid.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/aleph/sdk/connectors/superfluid.py b/src/aleph/sdk/connectors/superfluid.py index 76bbf907..90e74424 100644 --- a/src/aleph/sdk/connectors/superfluid.py +++ b/src/aleph/sdk/connectors/superfluid.py @@ -37,6 +37,21 @@ def __init__(self, account: ETHAccount): self.super_token = str(get_super_token_address(account.chain)) self.cfaV1Instance = CFA_V1(account.rpc, account.chain_id) + def _simulate_create_tx_flow(self, flow: Decimal, block=True) -> bool: + operation = self.cfaV1Instance.create_flow( + sender=self.normalized_address, + receiver=to_normalized_address( + "0x0000000000000000000000000000000000000001" + ), # Fake Address we do not sign/send this transactions + super_token=self.super_token, + flow_rate=int(to_wei_token(flow)), + ) + + populated_transaction = operation._get_populated_transaction_request( + self.account.rpc, self.account._account.key + ) + return self.account.can_transact(tx=populated_transaction, block=block) + async def _execute_operation_with_account(self, operation: Operation) -> str: """ Execute an operation using the provided ETHAccount @@ -51,7 +66,7 @@ async def _execute_operation_with_account(self, operation: Operation) -> str: def can_start_flow(self, flow: Decimal, block=True) -> bool: """Check if the account has enough funds to start a Superfluid flow of the given size.""" valid = False - if self.account.can_transact(block=block): + if self._simulate_create_tx_flow(flow=flow, block=block): balance = self.account.get_super_token_balance() MIN_FLOW_4H = to_wei_token(flow) * Decimal(self.MIN_4_HOURS) valid = balance > MIN_FLOW_4H From 5c4c543a6ceb6c3cc70cfd52fcb717dc48a104b3 Mon Sep 17 00:00:00 2001 From: 1yam Date: Mon, 31 Mar 2025 12:43:46 +0000 Subject: [PATCH 03/12] feat: add can_transact check in _execute_operation_with_account to prevent underfunded tx --- src/aleph/sdk/connectors/superfluid.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/aleph/sdk/connectors/superfluid.py b/src/aleph/sdk/connectors/superfluid.py index 90e74424..a2ca8bce 100644 --- a/src/aleph/sdk/connectors/superfluid.py +++ b/src/aleph/sdk/connectors/superfluid.py @@ -61,6 +61,8 @@ async def _execute_operation_with_account(self, operation: Operation) -> str: populated_transaction = operation._get_populated_transaction_request( self.account.rpc, self.account._account.key ) + self.account.can_transact(tx=populated_transaction) + return await self.account._sign_and_send_transaction(populated_transaction) def can_start_flow(self, flow: Decimal, block=True) -> bool: From f40392c1f58e785a6dfa79c5ae805481bfa1f9e9 Mon Sep 17 00:00:00 2001 From: 1yam Date: Mon, 31 Mar 2025 12:44:58 +0000 Subject: [PATCH 04/12] fix: remove unnecessary can_start_flow check in create_flow --- src/aleph/sdk/connectors/superfluid.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/aleph/sdk/connectors/superfluid.py b/src/aleph/sdk/connectors/superfluid.py index a2ca8bce..60fbe646 100644 --- a/src/aleph/sdk/connectors/superfluid.py +++ b/src/aleph/sdk/connectors/superfluid.py @@ -82,7 +82,6 @@ def can_start_flow(self, flow: Decimal, block=True) -> bool: async def create_flow(self, receiver: str, flow: Decimal) -> str: """Create a Superfluid flow between two addresses.""" - self.can_start_flow(flow) return await self._execute_operation_with_account( operation=self.cfaV1Instance.create_flow( sender=self.normalized_address, From 4ea99249edefe37ba38191598c79cc9733b98246 Mon Sep 17 00:00:00 2001 From: 1yam Date: Mon, 31 Mar 2025 12:59:02 +0000 Subject: [PATCH 05/12] fix: should use MIN_ETH_BALANCE_WEI instead of MIN_ETH_BALANCE --- src/aleph/sdk/chains/ethereum.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/aleph/sdk/chains/ethereum.py b/src/aleph/sdk/chains/ethereum.py index 2585fdcd..4d385150 100644 --- a/src/aleph/sdk/chains/ethereum.py +++ b/src/aleph/sdk/chains/ethereum.py @@ -21,7 +21,6 @@ from ..connectors.superfluid import Superfluid from ..evm_utils import ( BALANCEOF_ABI, - MIN_ETH_BALANCE, MIN_ETH_BALANCE_WEI, FlowUpdate, from_wei_token, @@ -125,7 +124,7 @@ def can_transact(self, tx: TxParams, block=True) -> bool: estimated_gas = self._provider.eth.estimate_gas(tx) except Exception: estimated_gas = ( - MIN_ETH_BALANCE # Fallback to MIN_ETH_BALANCE if estimation fails + MIN_ETH_BALANCE_WEI # Fallback to MIN_ETH_BALANCE if estimation fails ) valid = balance > estimated_gas if self.chain else False From 59c8c02d6701ecaa820874a7eac60f8a70c8d99d Mon Sep 17 00:00:00 2001 From: 1yam Date: Mon, 31 Mar 2025 13:03:54 +0000 Subject: [PATCH 06/12] fix: ensure _provider exist while using can_transact --- src/aleph/sdk/chains/ethereum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/aleph/sdk/chains/ethereum.py b/src/aleph/sdk/chains/ethereum.py index 4d385150..4e843f1a 100644 --- a/src/aleph/sdk/chains/ethereum.py +++ b/src/aleph/sdk/chains/ethereum.py @@ -121,6 +121,8 @@ def switch_chain(self, chain: Optional[Chain] = None): def can_transact(self, tx: TxParams, block=True) -> bool: balance = self.get_eth_balance() try: + assert self._provider is not None + estimated_gas = self._provider.eth.estimate_gas(tx) except Exception: estimated_gas = ( From ee33e52373937b91e04ef5a39119d98b30132e4c Mon Sep 17 00:00:00 2001 From: 1yam Date: Mon, 31 Mar 2025 14:59:50 +0000 Subject: [PATCH 07/12] fix: return false if error got returned while trying to estimate the gas cost in _simulate_create_tx_flow --- src/aleph/sdk/connectors/superfluid.py | 29 ++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/aleph/sdk/connectors/superfluid.py b/src/aleph/sdk/connectors/superfluid.py index 60fbe646..a2799331 100644 --- a/src/aleph/sdk/connectors/superfluid.py +++ b/src/aleph/sdk/connectors/superfluid.py @@ -38,19 +38,22 @@ def __init__(self, account: ETHAccount): self.cfaV1Instance = CFA_V1(account.rpc, account.chain_id) def _simulate_create_tx_flow(self, flow: Decimal, block=True) -> bool: - operation = self.cfaV1Instance.create_flow( - sender=self.normalized_address, - receiver=to_normalized_address( - "0x0000000000000000000000000000000000000001" - ), # Fake Address we do not sign/send this transactions - super_token=self.super_token, - flow_rate=int(to_wei_token(flow)), - ) - - populated_transaction = operation._get_populated_transaction_request( - self.account.rpc, self.account._account.key - ) - return self.account.can_transact(tx=populated_transaction, block=block) + try: + operation = self.cfaV1Instance.create_flow( + sender=self.normalized_address, + receiver=to_normalized_address( + "0x0000000000000000000000000000000000000001" + ), # Fake Address we do not sign/send this transactions + super_token=self.super_token, + flow_rate=int(to_wei_token(flow)), + ) + + populated_transaction = operation._get_populated_transaction_request( + self.account.rpc, self.account._account.key + ) + return self.account.can_transact(tx=populated_transaction, block=block) + except Exception: + return False async def _execute_operation_with_account(self, operation: Operation) -> str: """ From d8beeeba603f1d81c799e2e53f48a42ee9bd7e30 Mon Sep 17 00:00:00 2001 From: 1yam Date: Wed, 2 Apr 2025 13:54:23 +0000 Subject: [PATCH 08/12] Fix: gas estimations + error handling for gas / Aleph token --- src/aleph/sdk/chains/ethereum.py | 28 ++++++++++++++++++-------- src/aleph/sdk/connectors/superfluid.py | 25 +++++++++++------------ 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/aleph/sdk/chains/ethereum.py b/src/aleph/sdk/chains/ethereum.py index 4e843f1a..02bebd8f 100644 --- a/src/aleph/sdk/chains/ethereum.py +++ b/src/aleph/sdk/chains/ethereum.py @@ -11,6 +11,7 @@ from eth_keys.exceptions import BadSignature as EthBadSignatureError from superfluid import Web3FlowInfo from web3 import Web3 +from web3.exceptions import ContractCustomError from web3.middleware import ExtraDataToPOAMiddleware from web3.types import TxParams, TxReceipt @@ -119,22 +120,33 @@ def switch_chain(self, chain: Optional[Chain] = None): self.connect_chain(chain=chain) def can_transact(self, tx: TxParams, block=True) -> bool: - balance = self.get_eth_balance() + balance_wei = self.get_eth_balance() try: assert self._provider is not None estimated_gas = self._provider.eth.estimate_gas(tx) - except Exception: - estimated_gas = ( - MIN_ETH_BALANCE_WEI # Fallback to MIN_ETH_BALANCE if estimation fails - ) - valid = balance > estimated_gas if self.chain else False + gas_price = tx.get("gasPrice", self._provider.eth.gas_price) + + if "maxFeePerGas" in tx: + max_fee = tx["maxFeePerGas"] + total_fee_wei = estimated_gas * max_fee + else: + total_fee_wei = estimated_gas * gas_price + + total_fee_wei = int(total_fee_wei * 1.2) + + except ContractCustomError: + total_fee_wei = MIN_ETH_BALANCE_WEI # Fallback if estimation fails + + required_fee_wei = total_fee_wei + (tx.get("value", 0)) + + valid = balance_wei > required_fee_wei if self.chain else False if not valid and block: raise InsufficientFundsError( token_type=TokenType.GAS, - required_funds=estimated_gas, - available_funds=float(from_wei_token(balance)), + required_funds=float(from_wei_token(required_fee_wei)), + available_funds=float(from_wei_token(balance_wei)), ) return valid diff --git a/src/aleph/sdk/connectors/superfluid.py b/src/aleph/sdk/connectors/superfluid.py index a2799331..71a5b457 100644 --- a/src/aleph/sdk/connectors/superfluid.py +++ b/src/aleph/sdk/connectors/superfluid.py @@ -5,6 +5,7 @@ from eth_utils import to_normalized_address from superfluid import CFA_V1, Operation, Web3FlowInfo +from web3.exceptions import ContractCustomError from aleph.sdk.evm_utils import ( FlowUpdate, @@ -52,7 +53,15 @@ def _simulate_create_tx_flow(self, flow: Decimal, block=True) -> bool: self.account.rpc, self.account._account.key ) return self.account.can_transact(tx=populated_transaction, block=block) - except Exception: + except ContractCustomError as e: + if getattr(e, "data", None) == "0xea76c9b3": + balance = self.account.get_super_token_balance() + MIN_FLOW_4H = to_wei_token(flow) * Decimal(self.MIN_4_HOURS) + raise InsufficientFundsError( + token_type=TokenType.ALEPH, + required_funds=float(from_wei_token(MIN_FLOW_4H)), + available_funds=float(from_wei_token(balance)), + ) return False async def _execute_operation_with_account(self, operation: Operation) -> str: @@ -70,18 +79,8 @@ async def _execute_operation_with_account(self, operation: Operation) -> str: def can_start_flow(self, flow: Decimal, block=True) -> bool: """Check if the account has enough funds to start a Superfluid flow of the given size.""" - valid = False - if self._simulate_create_tx_flow(flow=flow, block=block): - balance = self.account.get_super_token_balance() - MIN_FLOW_4H = to_wei_token(flow) * Decimal(self.MIN_4_HOURS) - valid = balance > MIN_FLOW_4H - if not valid and block: - raise InsufficientFundsError( - token_type=TokenType.ALEPH, - required_funds=float(from_wei_token(MIN_FLOW_4H)), - available_funds=float(from_wei_token(balance)), - ) - return valid + return self._simulate_create_tx_flow(flow=flow, block=block) + async def create_flow(self, receiver: str, flow: Decimal) -> str: """Create a Superfluid flow between two addresses.""" From 4380c7485e14f7a62f729371984cedcd60d74959 Mon Sep 17 00:00:00 2001 From: 1yam Date: Wed, 2 Apr 2025 14:03:58 +0000 Subject: [PATCH 09/12] fix: linting error `hatch` --- src/aleph/sdk/connectors/superfluid.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/aleph/sdk/connectors/superfluid.py b/src/aleph/sdk/connectors/superfluid.py index 71a5b457..cd971b74 100644 --- a/src/aleph/sdk/connectors/superfluid.py +++ b/src/aleph/sdk/connectors/superfluid.py @@ -81,7 +81,6 @@ def can_start_flow(self, flow: Decimal, block=True) -> bool: """Check if the account has enough funds to start a Superfluid flow of the given size.""" return self._simulate_create_tx_flow(flow=flow, block=block) - async def create_flow(self, receiver: str, flow: Decimal) -> str: """Create a Superfluid flow between two addresses.""" return await self._execute_operation_with_account( From c5a356231d9741099f3f9a2288ad01652a18e2b6 Mon Sep 17 00:00:00 2001 From: 1yam Date: Thu, 15 May 2025 14:16:26 +0200 Subject: [PATCH 10/12] Feature: gas estimations unit test --- tests/unit/test_gas_estimation.py | 157 ++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 tests/unit/test_gas_estimation.py diff --git a/tests/unit/test_gas_estimation.py b/tests/unit/test_gas_estimation.py new file mode 100644 index 00000000..2cdb6861 --- /dev/null +++ b/tests/unit/test_gas_estimation.py @@ -0,0 +1,157 @@ +import pytest +from decimal import Decimal +from unittest.mock import MagicMock, patch + +from aleph_message.models import Chain +from web3.types import TxParams +from web3.exceptions import ContractCustomError + +from aleph.sdk.chains.ethereum import ETHAccount +from aleph.sdk.exceptions import InsufficientFundsError +from aleph.sdk.evm_utils import MIN_ETH_BALANCE_WEI +from aleph.sdk.connectors.superfluid import Superfluid +from aleph.sdk.types import TokenType + + +@pytest.fixture +def mock_eth_account(): + private_key = b"\x01" * 32 + account = ETHAccount( + private_key, + chain=Chain.ETH, + ) + account._provider = MagicMock() + account._provider.eth = MagicMock() + account._provider.eth.gas_price = 20_000_000_000 # 20 Gwei + account._provider.eth.estimate_gas = MagicMock(return_value=100_000) # 100k gas units + + # Mock get_eth_balance to return a specific balance + account.get_eth_balance = MagicMock(return_value=10**18) # 1 ETH + + return account + + +@pytest.fixture +def mock_superfluid(mock_eth_account): + superfluid = Superfluid(mock_eth_account) + superfluid.cfaV1Instance = MagicMock() + superfluid.cfaV1Instance.create_flow = MagicMock() + superfluid.super_token = "0xsupertokenaddress" + superfluid.normalized_address = "0xsenderaddress" + + # Mock the operation + operation = MagicMock() + operation._get_populated_transaction_request = MagicMock( + return_value={"value": 0, "gas": 100000, "gasPrice": 20_000_000_000} + ) + superfluid.cfaV1Instance.create_flow.return_value = operation + + return superfluid + + +class TestGasEstimation: + def test_can_transact_with_sufficient_funds(self, mock_eth_account): + tx = TxParams({"to": "0xreceiver", "value": 0}) + + # Should pass with 1 ETH balance against ~0.002 ETH gas cost + assert mock_eth_account.can_transact(tx=tx, block=True) is True + + def test_can_transact_with_insufficient_funds(self, mock_eth_account): + tx = TxParams({"to": "0xreceiver", "value": 0}) + + # Set balance to almost zero + mock_eth_account.get_eth_balance = MagicMock(return_value=1000) + + # Should raise InsufficientFundsError + with pytest.raises(InsufficientFundsError) as exc_info: + mock_eth_account.can_transact(tx=tx, block=True) + + assert exc_info.value.token_type == TokenType.GAS + + def test_can_transact_with_legacy_gas_price(self, mock_eth_account): + tx = TxParams({ + "to": "0xreceiver", + "value": 0, + "gasPrice": 30_000_000_000 # 30 Gwei + }) + + # Should use the tx's gasPrice instead of default + mock_eth_account.can_transact(tx=tx, block=True) + + # It should have used the tx's gasPrice for calculation + mock_eth_account._provider.eth.estimate_gas.assert_called_once() + + def test_can_transact_with_eip1559_gas(self, mock_eth_account): + tx = TxParams({ + "to": "0xreceiver", + "value": 0, + "maxFeePerGas": 40_000_000_000 # 40 Gwei + }) + + # Should use the tx's maxFeePerGas + mock_eth_account.can_transact(tx=tx, block=True) + + # It should have used the tx's maxFeePerGas for calculation + mock_eth_account._provider.eth.estimate_gas.assert_called_once() + + def test_can_transact_with_contract_error(self, mock_eth_account): + tx = TxParams({"to": "0xreceiver", "value": 0}) + + # Make estimate_gas throw a ContractCustomError + mock_eth_account._provider.eth.estimate_gas.side_effect = ContractCustomError("error") + + # Should fallback to MIN_ETH_BALANCE_WEI + mock_eth_account.can_transact(tx=tx, block=True) + + # It should have called estimate_gas + mock_eth_account._provider.eth.estimate_gas.assert_called_once() + + +class TestSuperfluidFlowEstimation: + @pytest.mark.asyncio + async def test_simulate_create_tx_flow_success(self, mock_superfluid, mock_eth_account): + # Patch the can_transact method to simulate a successful transaction + with patch.object(mock_eth_account, 'can_transact', return_value=True): + result = mock_superfluid._simulate_create_tx_flow(Decimal("0.00000005")) + assert result is True + + # Verify the flow was correctly simulated but not executed + mock_superfluid.cfaV1Instance.create_flow.assert_called_once() + assert "0x0000000000000000000000000000000000000001" in str(mock_superfluid.cfaV1Instance.create_flow.call_args) + + @pytest.mark.asyncio + async def test_simulate_create_tx_flow_contract_error(self, mock_superfluid, mock_eth_account): + # Setup a contract error code for insufficient deposit + error = ContractCustomError("Insufficient deposit") + error.data = "0xea76c9b3" # This is the specific error code checked in the code + + # Mock can_transact to throw the error + with patch.object(mock_eth_account, 'can_transact', side_effect=error): + # Also mock get_super_token_balance for the error case + with patch.object(mock_eth_account, 'get_super_token_balance', return_value=0): + # Should raise InsufficientFundsError for ALEPH token + with pytest.raises(InsufficientFundsError) as exc_info: + mock_superfluid._simulate_create_tx_flow(Decimal("0.00000005")) + + assert exc_info.value.token_type == TokenType.ALEPH + + @pytest.mark.asyncio + async def test_simulate_create_tx_flow_other_error(self, mock_superfluid, mock_eth_account): + # Setup a different contract error code + error = ContractCustomError("Other error") + error.data = "0xsomeothercode" + + # Mock can_transact to throw the error + with patch.object(mock_eth_account, 'can_transact', side_effect=error): + # Should return False for other errors + result = mock_superfluid._simulate_create_tx_flow(Decimal("0.00000005")) + assert result is False + + @pytest.mark.asyncio + async def test_can_start_flow_uses_simulation(self, mock_superfluid): + # Mock _simulate_create_tx_flow to verify it's called + with patch.object(mock_superfluid, '_simulate_create_tx_flow', return_value=True) as mock_simulate: + result = mock_superfluid.can_start_flow(Decimal("0.00000005")) + + assert result is True + mock_simulate.assert_called_once_with(flow=Decimal("0.00000005"), block=True) \ No newline at end of file From 7845ad30670861258249c5ef7c17f9c0a82d10c6 Mon Sep 17 00:00:00 2001 From: 1yam Date: Thu, 15 May 2025 14:22:51 +0200 Subject: [PATCH 11/12] fix: linting --- tests/unit/test_gas_estimation.py | 119 +++++++++++++++++------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/tests/unit/test_gas_estimation.py b/tests/unit/test_gas_estimation.py index 2cdb6861..0ea8baeb 100644 --- a/tests/unit/test_gas_estimation.py +++ b/tests/unit/test_gas_estimation.py @@ -1,15 +1,14 @@ -import pytest from decimal import Decimal from unittest.mock import MagicMock, patch +import pytest from aleph_message.models import Chain -from web3.types import TxParams from web3.exceptions import ContractCustomError +from web3.types import TxParams from aleph.sdk.chains.ethereum import ETHAccount -from aleph.sdk.exceptions import InsufficientFundsError -from aleph.sdk.evm_utils import MIN_ETH_BALANCE_WEI from aleph.sdk.connectors.superfluid import Superfluid +from aleph.sdk.exceptions import InsufficientFundsError from aleph.sdk.types import TokenType @@ -23,11 +22,13 @@ def mock_eth_account(): account._provider = MagicMock() account._provider.eth = MagicMock() account._provider.eth.gas_price = 20_000_000_000 # 20 Gwei - account._provider.eth.estimate_gas = MagicMock(return_value=100_000) # 100k gas units - + account._provider.eth.estimate_gas = MagicMock( + return_value=100_000 + ) # 100k gas units + # Mock get_eth_balance to return a specific balance account.get_eth_balance = MagicMock(return_value=10**18) # 1 ETH - + return account @@ -38,120 +39,132 @@ def mock_superfluid(mock_eth_account): superfluid.cfaV1Instance.create_flow = MagicMock() superfluid.super_token = "0xsupertokenaddress" superfluid.normalized_address = "0xsenderaddress" - + # Mock the operation operation = MagicMock() operation._get_populated_transaction_request = MagicMock( return_value={"value": 0, "gas": 100000, "gasPrice": 20_000_000_000} ) superfluid.cfaV1Instance.create_flow.return_value = operation - + return superfluid class TestGasEstimation: def test_can_transact_with_sufficient_funds(self, mock_eth_account): tx = TxParams({"to": "0xreceiver", "value": 0}) - + # Should pass with 1 ETH balance against ~0.002 ETH gas cost assert mock_eth_account.can_transact(tx=tx, block=True) is True - + def test_can_transact_with_insufficient_funds(self, mock_eth_account): tx = TxParams({"to": "0xreceiver", "value": 0}) - + # Set balance to almost zero mock_eth_account.get_eth_balance = MagicMock(return_value=1000) - + # Should raise InsufficientFundsError with pytest.raises(InsufficientFundsError) as exc_info: mock_eth_account.can_transact(tx=tx, block=True) - + assert exc_info.value.token_type == TokenType.GAS - + def test_can_transact_with_legacy_gas_price(self, mock_eth_account): - tx = TxParams({ - "to": "0xreceiver", - "value": 0, - "gasPrice": 30_000_000_000 # 30 Gwei - }) - + tx = TxParams( + {"to": "0xreceiver", "value": 0, "gasPrice": 30_000_000_000} # 30 Gwei + ) + # Should use the tx's gasPrice instead of default mock_eth_account.can_transact(tx=tx, block=True) - + # It should have used the tx's gasPrice for calculation mock_eth_account._provider.eth.estimate_gas.assert_called_once() - + def test_can_transact_with_eip1559_gas(self, mock_eth_account): - tx = TxParams({ - "to": "0xreceiver", - "value": 0, - "maxFeePerGas": 40_000_000_000 # 40 Gwei - }) - + tx = TxParams( + {"to": "0xreceiver", "value": 0, "maxFeePerGas": 40_000_000_000} # 40 Gwei + ) + # Should use the tx's maxFeePerGas mock_eth_account.can_transact(tx=tx, block=True) - + # It should have used the tx's maxFeePerGas for calculation mock_eth_account._provider.eth.estimate_gas.assert_called_once() - + def test_can_transact_with_contract_error(self, mock_eth_account): tx = TxParams({"to": "0xreceiver", "value": 0}) - + # Make estimate_gas throw a ContractCustomError - mock_eth_account._provider.eth.estimate_gas.side_effect = ContractCustomError("error") - + mock_eth_account._provider.eth.estimate_gas.side_effect = ContractCustomError( + "error" + ) + # Should fallback to MIN_ETH_BALANCE_WEI mock_eth_account.can_transact(tx=tx, block=True) - + # It should have called estimate_gas mock_eth_account._provider.eth.estimate_gas.assert_called_once() class TestSuperfluidFlowEstimation: @pytest.mark.asyncio - async def test_simulate_create_tx_flow_success(self, mock_superfluid, mock_eth_account): + async def test_simulate_create_tx_flow_success( + self, mock_superfluid, mock_eth_account + ): # Patch the can_transact method to simulate a successful transaction - with patch.object(mock_eth_account, 'can_transact', return_value=True): + with patch.object(mock_eth_account, "can_transact", return_value=True): result = mock_superfluid._simulate_create_tx_flow(Decimal("0.00000005")) assert result is True - + # Verify the flow was correctly simulated but not executed mock_superfluid.cfaV1Instance.create_flow.assert_called_once() - assert "0x0000000000000000000000000000000000000001" in str(mock_superfluid.cfaV1Instance.create_flow.call_args) - + assert "0x0000000000000000000000000000000000000001" in str( + mock_superfluid.cfaV1Instance.create_flow.call_args + ) + @pytest.mark.asyncio - async def test_simulate_create_tx_flow_contract_error(self, mock_superfluid, mock_eth_account): + async def test_simulate_create_tx_flow_contract_error( + self, mock_superfluid, mock_eth_account + ): # Setup a contract error code for insufficient deposit error = ContractCustomError("Insufficient deposit") error.data = "0xea76c9b3" # This is the specific error code checked in the code - + # Mock can_transact to throw the error - with patch.object(mock_eth_account, 'can_transact', side_effect=error): + with patch.object(mock_eth_account, "can_transact", side_effect=error): # Also mock get_super_token_balance for the error case - with patch.object(mock_eth_account, 'get_super_token_balance', return_value=0): + with patch.object( + mock_eth_account, "get_super_token_balance", return_value=0 + ): # Should raise InsufficientFundsError for ALEPH token with pytest.raises(InsufficientFundsError) as exc_info: mock_superfluid._simulate_create_tx_flow(Decimal("0.00000005")) - + assert exc_info.value.token_type == TokenType.ALEPH - + @pytest.mark.asyncio - async def test_simulate_create_tx_flow_other_error(self, mock_superfluid, mock_eth_account): + async def test_simulate_create_tx_flow_other_error( + self, mock_superfluid, mock_eth_account + ): # Setup a different contract error code error = ContractCustomError("Other error") error.data = "0xsomeothercode" - + # Mock can_transact to throw the error - with patch.object(mock_eth_account, 'can_transact', side_effect=error): + with patch.object(mock_eth_account, "can_transact", side_effect=error): # Should return False for other errors result = mock_superfluid._simulate_create_tx_flow(Decimal("0.00000005")) assert result is False - + @pytest.mark.asyncio async def test_can_start_flow_uses_simulation(self, mock_superfluid): # Mock _simulate_create_tx_flow to verify it's called - with patch.object(mock_superfluid, '_simulate_create_tx_flow', return_value=True) as mock_simulate: + with patch.object( + mock_superfluid, "_simulate_create_tx_flow", return_value=True + ) as mock_simulate: result = mock_superfluid.can_start_flow(Decimal("0.00000005")) - + assert result is True - mock_simulate.assert_called_once_with(flow=Decimal("0.00000005"), block=True) \ No newline at end of file + mock_simulate.assert_called_once_with( + flow=Decimal("0.00000005"), block=True + ) From 286562817ecc4f9121f12ad578ad826471295362 Mon Sep 17 00:00:00 2001 From: 1yam Date: Thu, 15 May 2025 14:26:53 +0200 Subject: [PATCH 12/12] fix: mypy cannot assign method --- tests/unit/test_gas_estimation.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_gas_estimation.py b/tests/unit/test_gas_estimation.py index 0ea8baeb..abbd8c5c 100644 --- a/tests/unit/test_gas_estimation.py +++ b/tests/unit/test_gas_estimation.py @@ -27,9 +27,8 @@ def mock_eth_account(): ) # 100k gas units # Mock get_eth_balance to return a specific balance - account.get_eth_balance = MagicMock(return_value=10**18) # 1 ETH - - return account + with patch.object(account, "get_eth_balance", return_value=10**18): # 1 ETH + yield account @pytest.fixture @@ -61,11 +60,10 @@ def test_can_transact_with_insufficient_funds(self, mock_eth_account): tx = TxParams({"to": "0xreceiver", "value": 0}) # Set balance to almost zero - mock_eth_account.get_eth_balance = MagicMock(return_value=1000) - - # Should raise InsufficientFundsError - with pytest.raises(InsufficientFundsError) as exc_info: - mock_eth_account.can_transact(tx=tx, block=True) + with patch.object(mock_eth_account, "get_eth_balance", return_value=1000): + # Should raise InsufficientFundsError + with pytest.raises(InsufficientFundsError) as exc_info: + mock_eth_account.can_transact(tx=tx, block=True) assert exc_info.value.token_type == TokenType.GAS