Skip to content

Commit 0a8039b

Browse files
committed
Feature: gas estimations unit test
1 parent d1920ab commit 0a8039b

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed

tests/unit/test_gas_estimation.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import pytest
2+
from decimal import Decimal
3+
from unittest.mock import MagicMock, patch
4+
5+
from aleph_message.models import Chain
6+
from web3.types import TxParams
7+
from web3.exceptions import ContractCustomError
8+
9+
from aleph.sdk.chains.ethereum import ETHAccount
10+
from aleph.sdk.exceptions import InsufficientFundsError
11+
from aleph.sdk.evm_utils import MIN_ETH_BALANCE_WEI
12+
from aleph.sdk.connectors.superfluid import Superfluid
13+
from aleph.sdk.types import TokenType
14+
15+
16+
@pytest.fixture
17+
def mock_eth_account():
18+
private_key = b"\x01" * 32
19+
account = ETHAccount(
20+
private_key,
21+
chain=Chain.ETH,
22+
)
23+
account._provider = MagicMock()
24+
account._provider.eth = MagicMock()
25+
account._provider.eth.gas_price = 20_000_000_000 # 20 Gwei
26+
account._provider.eth.estimate_gas = MagicMock(return_value=100_000) # 100k gas units
27+
28+
# Mock get_eth_balance to return a specific balance
29+
account.get_eth_balance = MagicMock(return_value=10**18) # 1 ETH
30+
31+
return account
32+
33+
34+
@pytest.fixture
35+
def mock_superfluid(mock_eth_account):
36+
superfluid = Superfluid(mock_eth_account)
37+
superfluid.cfaV1Instance = MagicMock()
38+
superfluid.cfaV1Instance.create_flow = MagicMock()
39+
superfluid.super_token = "0xsupertokenaddress"
40+
superfluid.normalized_address = "0xsenderaddress"
41+
42+
# Mock the operation
43+
operation = MagicMock()
44+
operation._get_populated_transaction_request = MagicMock(
45+
return_value={"value": 0, "gas": 100000, "gasPrice": 20_000_000_000}
46+
)
47+
superfluid.cfaV1Instance.create_flow.return_value = operation
48+
49+
return superfluid
50+
51+
52+
class TestGasEstimation:
53+
def test_can_transact_with_sufficient_funds(self, mock_eth_account):
54+
tx = TxParams({"to": "0xreceiver", "value": 0})
55+
56+
# Should pass with 1 ETH balance against ~0.002 ETH gas cost
57+
assert mock_eth_account.can_transact(tx=tx, block=True) is True
58+
59+
def test_can_transact_with_insufficient_funds(self, mock_eth_account):
60+
tx = TxParams({"to": "0xreceiver", "value": 0})
61+
62+
# Set balance to almost zero
63+
mock_eth_account.get_eth_balance = MagicMock(return_value=1000)
64+
65+
# Should raise InsufficientFundsError
66+
with pytest.raises(InsufficientFundsError) as exc_info:
67+
mock_eth_account.can_transact(tx=tx, block=True)
68+
69+
assert exc_info.value.token_type == TokenType.GAS
70+
71+
def test_can_transact_with_legacy_gas_price(self, mock_eth_account):
72+
tx = TxParams({
73+
"to": "0xreceiver",
74+
"value": 0,
75+
"gasPrice": 30_000_000_000 # 30 Gwei
76+
})
77+
78+
# Should use the tx's gasPrice instead of default
79+
mock_eth_account.can_transact(tx=tx, block=True)
80+
81+
# It should have used the tx's gasPrice for calculation
82+
mock_eth_account._provider.eth.estimate_gas.assert_called_once()
83+
84+
def test_can_transact_with_eip1559_gas(self, mock_eth_account):
85+
tx = TxParams({
86+
"to": "0xreceiver",
87+
"value": 0,
88+
"maxFeePerGas": 40_000_000_000 # 40 Gwei
89+
})
90+
91+
# Should use the tx's maxFeePerGas
92+
mock_eth_account.can_transact(tx=tx, block=True)
93+
94+
# It should have used the tx's maxFeePerGas for calculation
95+
mock_eth_account._provider.eth.estimate_gas.assert_called_once()
96+
97+
def test_can_transact_with_contract_error(self, mock_eth_account):
98+
tx = TxParams({"to": "0xreceiver", "value": 0})
99+
100+
# Make estimate_gas throw a ContractCustomError
101+
mock_eth_account._provider.eth.estimate_gas.side_effect = ContractCustomError("error")
102+
103+
# Should fallback to MIN_ETH_BALANCE_WEI
104+
mock_eth_account.can_transact(tx=tx, block=True)
105+
106+
# It should have called estimate_gas
107+
mock_eth_account._provider.eth.estimate_gas.assert_called_once()
108+
109+
110+
class TestSuperfluidFlowEstimation:
111+
@pytest.mark.asyncio
112+
async def test_simulate_create_tx_flow_success(self, mock_superfluid, mock_eth_account):
113+
# Patch the can_transact method to simulate a successful transaction
114+
with patch.object(mock_eth_account, 'can_transact', return_value=True):
115+
result = mock_superfluid._simulate_create_tx_flow(Decimal("0.00000005"))
116+
assert result is True
117+
118+
# Verify the flow was correctly simulated but not executed
119+
mock_superfluid.cfaV1Instance.create_flow.assert_called_once()
120+
assert "0x0000000000000000000000000000000000000001" in str(mock_superfluid.cfaV1Instance.create_flow.call_args)
121+
122+
@pytest.mark.asyncio
123+
async def test_simulate_create_tx_flow_contract_error(self, mock_superfluid, mock_eth_account):
124+
# Setup a contract error code for insufficient deposit
125+
error = ContractCustomError("Insufficient deposit")
126+
error.data = "0xea76c9b3" # This is the specific error code checked in the code
127+
128+
# Mock can_transact to throw the error
129+
with patch.object(mock_eth_account, 'can_transact', side_effect=error):
130+
# Also mock get_super_token_balance for the error case
131+
with patch.object(mock_eth_account, 'get_super_token_balance', return_value=0):
132+
# Should raise InsufficientFundsError for ALEPH token
133+
with pytest.raises(InsufficientFundsError) as exc_info:
134+
mock_superfluid._simulate_create_tx_flow(Decimal("0.00000005"))
135+
136+
assert exc_info.value.token_type == TokenType.ALEPH
137+
138+
@pytest.mark.asyncio
139+
async def test_simulate_create_tx_flow_other_error(self, mock_superfluid, mock_eth_account):
140+
# Setup a different contract error code
141+
error = ContractCustomError("Other error")
142+
error.data = "0xsomeothercode"
143+
144+
# Mock can_transact to throw the error
145+
with patch.object(mock_eth_account, 'can_transact', side_effect=error):
146+
# Should return False for other errors
147+
result = mock_superfluid._simulate_create_tx_flow(Decimal("0.00000005"))
148+
assert result is False
149+
150+
@pytest.mark.asyncio
151+
async def test_can_start_flow_uses_simulation(self, mock_superfluid):
152+
# Mock _simulate_create_tx_flow to verify it's called
153+
with patch.object(mock_superfluid, '_simulate_create_tx_flow', return_value=True) as mock_simulate:
154+
result = mock_superfluid.can_start_flow(Decimal("0.00000005"))
155+
156+
assert result is True
157+
mock_simulate.assert_called_once_with(flow=Decimal("0.00000005"), block=True)

0 commit comments

Comments
 (0)