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