iwa 0.0.0__py3-none-any.whl → 0.0.1a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,1285 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
# Mock cowdao_cowpy before importing Wallet
|
|
7
|
+
sys.modules["cowdao_cowpy"] = MagicMock()
|
|
8
|
+
sys.modules["cowdao_cowpy.app_data"] = MagicMock()
|
|
9
|
+
sys.modules["cowdao_cowpy.app_data.utils"] = MagicMock()
|
|
10
|
+
sys.modules["cowdao_cowpy.common"] = MagicMock()
|
|
11
|
+
sys.modules["cowdao_cowpy.common.chains"] = MagicMock()
|
|
12
|
+
sys.modules["cowdao_cowpy.contracts"] = MagicMock()
|
|
13
|
+
sys.modules["cowdao_cowpy.contracts.order"] = MagicMock()
|
|
14
|
+
sys.modules["cowdao_cowpy.contracts.sign"] = MagicMock()
|
|
15
|
+
sys.modules["cowdao_cowpy.cow"] = MagicMock()
|
|
16
|
+
sys.modules["cowdao_cowpy.cow.swap"] = MagicMock()
|
|
17
|
+
sys.modules["cowdao_cowpy.order_book"] = MagicMock()
|
|
18
|
+
sys.modules["cowdao_cowpy.order_book.api"] = MagicMock()
|
|
19
|
+
sys.modules["cowdao_cowpy.order_book.config"] = MagicMock()
|
|
20
|
+
sys.modules["cowdao_cowpy.order_book.generated"] = MagicMock()
|
|
21
|
+
sys.modules["cowdao_cowpy.order_book.generated.model"] = MagicMock()
|
|
22
|
+
|
|
23
|
+
from iwa.core.chain import Gnosis
|
|
24
|
+
from iwa.core.models import StoredAccount, StoredSafeAccount
|
|
25
|
+
from iwa.core.services import TransferService
|
|
26
|
+
from iwa.core.wallet import Wallet
|
|
27
|
+
from iwa.plugins.gnosis.cow import OrderType
|
|
28
|
+
|
|
29
|
+
# Use valid addresses
|
|
30
|
+
VALID_ADDR_1 = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
|
|
31
|
+
VALID_ADDR_2 = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def mock_transaction_service():
|
|
36
|
+
with patch("iwa.core.wallet.TransactionService") as mock:
|
|
37
|
+
instance = mock.return_value
|
|
38
|
+
instance.sign_and_send.return_value = (True, "0xTxHash")
|
|
39
|
+
yield instance
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def mock_key_storage():
|
|
44
|
+
with patch("iwa.core.wallet.KeyStorage") as mock:
|
|
45
|
+
instance = mock.return_value
|
|
46
|
+
instance.accounts = {}
|
|
47
|
+
instance.get_account.return_value = None
|
|
48
|
+
instance.find_stored_account = instance.get_account
|
|
49
|
+
yield instance
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def mock_chain_interfaces():
|
|
54
|
+
with (
|
|
55
|
+
patch("iwa.core.chain.ChainInterfaces") as mock,
|
|
56
|
+
patch("iwa.core.services.transfer.multisend.ChainInterfaces", new=mock),
|
|
57
|
+
patch("iwa.core.services.transfer.erc20.ChainInterfaces", new=mock),
|
|
58
|
+
patch("iwa.core.services.transfer.native.ChainInterfaces", new=mock),
|
|
59
|
+
patch("iwa.core.services.transfer.base.ChainInterfaces", new=mock),
|
|
60
|
+
patch("iwa.core.services.balance.ChainInterfaces", new=mock),
|
|
61
|
+
patch("iwa.core.services.transaction.ChainInterfaces", new=mock),
|
|
62
|
+
patch("iwa.core.services.transfer.ChainInterfaces", new=mock),
|
|
63
|
+
# Patch ERC20Contract where it is imported in the transfer package __init__
|
|
64
|
+
patch("iwa.core.services.transfer.ERC20Contract"),
|
|
65
|
+
):
|
|
66
|
+
instance = mock.return_value
|
|
67
|
+
gnosis_interface = MagicMock()
|
|
68
|
+
|
|
69
|
+
# Use a mock for the chain instead of the real Gnosis object
|
|
70
|
+
mock_chain = MagicMock()
|
|
71
|
+
mock_chain.name = "Gnosis"
|
|
72
|
+
mock_chain.native_currency = "xDAI"
|
|
73
|
+
mock_chain.chain_id = 100
|
|
74
|
+
mock_chain.tokens = {}
|
|
75
|
+
|
|
76
|
+
def debug_get_token(name):
|
|
77
|
+
addr = mock_chain.tokens.get(name)
|
|
78
|
+
# print(f"DEBUG LAMBDA: name={name} tokens={mock_chain.tokens} addr={addr}")
|
|
79
|
+
return addr
|
|
80
|
+
|
|
81
|
+
mock_chain.get_token_address.side_effect = debug_get_token
|
|
82
|
+
gnosis_interface.chain = mock_chain
|
|
83
|
+
|
|
84
|
+
gnosis_interface.web3 = MagicMock()
|
|
85
|
+
gnosis_interface.web3.to_wei.side_effect = lambda val, unit: int(float(val) * 10**18)
|
|
86
|
+
gnosis_interface.web3.from_wei.side_effect = lambda val, unit: float(val) / 10**18
|
|
87
|
+
gnosis_interface.web3.eth.gas_price = 1000000000
|
|
88
|
+
gnosis_interface.get_erc20_allowance.return_value = 0
|
|
89
|
+
|
|
90
|
+
instance.get.return_value = gnosis_interface
|
|
91
|
+
yield instance
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.fixture
|
|
95
|
+
def mock_cow_swap():
|
|
96
|
+
with patch("iwa.core.services.transfer.swap.CowSwap") as mock:
|
|
97
|
+
yield mock
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.fixture
|
|
101
|
+
def mock_account_service(mock_key_storage):
|
|
102
|
+
with patch("iwa.core.wallet.AccountService") as mock:
|
|
103
|
+
instance = mock.return_value
|
|
104
|
+
instance.key_storage = mock_key_storage
|
|
105
|
+
instance.master_account = None
|
|
106
|
+
instance.get_account_data.return_value = {}
|
|
107
|
+
# Delegate to key_storage.get_account for compatibility
|
|
108
|
+
instance.resolve_account.side_effect = lambda tag: mock_key_storage.get_account(tag)
|
|
109
|
+
|
|
110
|
+
# Default get_token_address to look up in chain tokens
|
|
111
|
+
def get_token_address_side_effect(name, chain):
|
|
112
|
+
if name == "native":
|
|
113
|
+
return "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
|
114
|
+
if str(name).startswith("0x"):
|
|
115
|
+
return name
|
|
116
|
+
if chain:
|
|
117
|
+
return chain.get_token_address(name)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
instance.get_token_address.side_effect = get_token_address_side_effect
|
|
121
|
+
yield instance
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.fixture
|
|
125
|
+
def mock_balance_service(mock_key_storage, mock_account_service):
|
|
126
|
+
with patch("iwa.core.wallet.BalanceService") as mock:
|
|
127
|
+
instance = mock.return_value
|
|
128
|
+
# Ensure we don't return MagicMocks for numeric values by default
|
|
129
|
+
instance.get_native_balance_eth.return_value = 0.0
|
|
130
|
+
instance.get_native_balance_wei.return_value = 0
|
|
131
|
+
instance.get_erc20_balance_eth.return_value = 0.0
|
|
132
|
+
instance.get_erc20_balance_wei.return_value = 0
|
|
133
|
+
|
|
134
|
+
# Mocking resolve_account to return something with an address
|
|
135
|
+
def resolve_side_effect(tag_or_addr):
|
|
136
|
+
m = MagicMock()
|
|
137
|
+
m.address = VALID_ADDR_1 if not str(tag_or_addr).startswith("0x") else tag_or_addr
|
|
138
|
+
m.tag = "mock-tag"
|
|
139
|
+
return m
|
|
140
|
+
|
|
141
|
+
yield instance
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# NOTE: mock_safe_multisig_global was removed because SafeMultisig is no longer
|
|
145
|
+
# imported in TransferService. Safe transactions now go through SafeService.execute_safe_transaction().
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.fixture(autouse=True)
|
|
149
|
+
def mock_erc20_contract_global():
|
|
150
|
+
with (
|
|
151
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as m1,
|
|
152
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract") as m2,
|
|
153
|
+
patch("iwa.core.services.balance.ERC20Contract") as m3,
|
|
154
|
+
):
|
|
155
|
+
m1.return_value.decimals = 18
|
|
156
|
+
m2.return_value.decimals = 18
|
|
157
|
+
m3.return_value.decimals = 18
|
|
158
|
+
yield
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@pytest.fixture(autouse=True)
|
|
162
|
+
def mock_security_validations():
|
|
163
|
+
"""Mock security validations to allow tests to run without full config.
|
|
164
|
+
|
|
165
|
+
The _is_whitelisted_destination and _is_supported_token methods are mocked
|
|
166
|
+
to return True by default. Tests that specifically test security rejection
|
|
167
|
+
should patch these again to return False.
|
|
168
|
+
"""
|
|
169
|
+
with (
|
|
170
|
+
patch(
|
|
171
|
+
"iwa.core.services.transfer.TransferService._is_whitelisted_destination",
|
|
172
|
+
return_value=True,
|
|
173
|
+
),
|
|
174
|
+
patch(
|
|
175
|
+
"iwa.core.services.transfer.TransferService._is_supported_token",
|
|
176
|
+
return_value=True,
|
|
177
|
+
),
|
|
178
|
+
):
|
|
179
|
+
yield
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.fixture
|
|
183
|
+
def mock_safe_service(mock_key_storage, mock_account_service):
|
|
184
|
+
with patch("iwa.core.wallet.SafeService") as mock:
|
|
185
|
+
instance = mock.return_value
|
|
186
|
+
instance.key_storage = mock_key_storage
|
|
187
|
+
instance.account_service = mock_account_service
|
|
188
|
+
yield instance
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@pytest.fixture
|
|
192
|
+
def wallet(
|
|
193
|
+
mock_key_storage,
|
|
194
|
+
mock_chain_interfaces,
|
|
195
|
+
mock_cow_swap,
|
|
196
|
+
mock_account_service,
|
|
197
|
+
mock_balance_service,
|
|
198
|
+
mock_safe_service,
|
|
199
|
+
mock_transaction_service,
|
|
200
|
+
):
|
|
201
|
+
with patch("iwa.core.wallet.init_db"):
|
|
202
|
+
w = Wallet()
|
|
203
|
+
w.key_storage = mock_key_storage
|
|
204
|
+
w.account_service = mock_account_service
|
|
205
|
+
w.transaction_service = mock_transaction_service
|
|
206
|
+
w.balance_service = mock_balance_service
|
|
207
|
+
w.safe_service = mock_safe_service
|
|
208
|
+
|
|
209
|
+
# Re-initialize TransferService with these mocks
|
|
210
|
+
w.transfer_service = TransferService(
|
|
211
|
+
w.key_storage,
|
|
212
|
+
w.account_service,
|
|
213
|
+
w.balance_service,
|
|
214
|
+
w.safe_service,
|
|
215
|
+
w.transaction_service,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Mock internal transfer service methods to return 0 by default for numeric comparisons,
|
|
219
|
+
# but allow side_effect to handle tests that expect None.
|
|
220
|
+
def get_allowance_side_effect(
|
|
221
|
+
owner_address_or_tag,
|
|
222
|
+
spender_address,
|
|
223
|
+
token_address_or_name,
|
|
224
|
+
chain_name="gnosis",
|
|
225
|
+
):
|
|
226
|
+
if owner_address_or_tag == "unknown" or token_address_or_name == "INVALID":
|
|
227
|
+
return None
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
w.transfer_service.get_erc20_allowance = MagicMock(side_effect=get_allowance_side_effect)
|
|
231
|
+
w.transfer_service.get_native_balance_wei = MagicMock(return_value=0)
|
|
232
|
+
w.transfer_service.get_erc20_balance_wei = MagicMock(return_value=0)
|
|
233
|
+
yield w
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_wallet_init(wallet, mock_key_storage):
|
|
237
|
+
assert wallet.key_storage == mock_key_storage
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_get_token_address_native(wallet, mock_account_service):
|
|
241
|
+
chain = Gnosis()
|
|
242
|
+
mock_account_service.get_token_address.return_value = (
|
|
243
|
+
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
|
244
|
+
)
|
|
245
|
+
addr = wallet.get_token_address("native", chain)
|
|
246
|
+
assert addr == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_get_token_address_valid_address(wallet):
|
|
250
|
+
chain = Gnosis()
|
|
251
|
+
addr = wallet.get_token_address(VALID_ADDR_1, chain)
|
|
252
|
+
assert addr == VALID_ADDR_1
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_get_token_address_by_name(wallet):
|
|
256
|
+
chain = Gnosis()
|
|
257
|
+
# Assuming OLAS is in Gnosis tokens
|
|
258
|
+
addr = wallet.get_token_address("OLAS", chain)
|
|
259
|
+
assert addr == chain.tokens["OLAS"]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_get_token_address_invalid(wallet):
|
|
263
|
+
chain = Gnosis()
|
|
264
|
+
addr = wallet.get_token_address("INVALID_TOKEN", chain)
|
|
265
|
+
assert addr is None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_sign_and_send_transaction_account_not_found(wallet, mock_account_service):
|
|
269
|
+
mock_account_service.resolve_account.return_value = None
|
|
270
|
+
wallet.transaction_service.sign_and_send.return_value = (False, {})
|
|
271
|
+
|
|
272
|
+
success, receipt = wallet.sign_and_send_transaction({"to": "0x123"}, "unknown-tag", "gnosis")
|
|
273
|
+
|
|
274
|
+
assert success is False
|
|
275
|
+
assert receipt == {}
|
|
276
|
+
wallet.transaction_service.sign_and_send.assert_called_with(
|
|
277
|
+
{"to": "0x123"}, "unknown-tag", "gnosis", None
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_sign_and_send_transaction_success(wallet, mock_key_storage):
|
|
282
|
+
tx = {"to": "0x123", "value": 100}
|
|
283
|
+
|
|
284
|
+
# Setup mocks
|
|
285
|
+
wallet.transaction_service.sign_and_send.return_value = (True, {"status": 1})
|
|
286
|
+
|
|
287
|
+
# Call
|
|
288
|
+
success, receipt = wallet.sign_and_send_transaction(tx, "tag")
|
|
289
|
+
|
|
290
|
+
# Assert
|
|
291
|
+
assert success is True
|
|
292
|
+
assert receipt["status"] == 1
|
|
293
|
+
wallet.transaction_service.sign_and_send.assert_called_with(tx, "tag", "gnosis", None)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_get_accounts_balances(wallet, mock_key_storage, mock_chain_interfaces):
|
|
297
|
+
wallet.account_service.get_account_data.return_value = {"0x123": {}}
|
|
298
|
+
mock_chain_interfaces.get.return_value.get_balance.return_value = 100
|
|
299
|
+
wallet.balance_service.get_native_balance_eth.return_value = 1.0
|
|
300
|
+
|
|
301
|
+
accounts_data, token_balances = wallet.get_accounts_balances("gnosis", ["native"])
|
|
302
|
+
|
|
303
|
+
assert accounts_data == {"0x123": {}}
|
|
304
|
+
assert token_balances == {"0x123": {"native": 1.0}}
|
|
305
|
+
wallet.balance_service.get_native_balance_eth.assert_called_with("0x123", "gnosis")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_get_native_balance_eth(wallet, mock_chain_interfaces, mock_balance_service):
|
|
309
|
+
# chain_interface.get_native_balance_eth.return_value = 1.5 # Ignored
|
|
310
|
+
mock_balance_service.get_native_balance_eth.return_value = 1.5
|
|
311
|
+
|
|
312
|
+
balance = wallet.get_native_balance_eth(VALID_ADDR_2)
|
|
313
|
+
assert balance == 1.5
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_get_native_balance_wei(wallet, mock_chain_interfaces, mock_balance_service):
|
|
317
|
+
# chain_interface.get_native_balance_wei.return_value = ... # Ignored
|
|
318
|
+
mock_balance_service.get_native_balance_wei.return_value = 1500000000000000000
|
|
319
|
+
|
|
320
|
+
balance = wallet.get_native_balance_wei(VALID_ADDR_2)
|
|
321
|
+
assert balance == 1500000000000000000
|
|
322
|
+
# chain_interface.get_native_balance_wei.assert_called_with(VALID_ADDR_2) # Wrapper verification?
|
|
323
|
+
# If using MockBalanceService, ChainInterface is NOT called.
|
|
324
|
+
# So this assertion should be removed or changed to check BalanceService call.
|
|
325
|
+
mock_balance_service.get_native_balance_wei.assert_called_with(
|
|
326
|
+
VALID_ADDR_2, "gnosis"
|
|
327
|
+
) # Defaults to gnosis in test?
|
|
328
|
+
# Wallet.get_native_balance_wei takes (account_tag, chain_name="gnosis").
|
|
329
|
+
# If validation passes.
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_send_native_success(wallet, mock_key_storage, mock_chain_interfaces, mock_balance_service):
|
|
333
|
+
account = StoredAccount(address=VALID_ADDR_1, tag="sender")
|
|
334
|
+
mock_key_storage.get_account.return_value = account
|
|
335
|
+
|
|
336
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
337
|
+
# chain_interface.get_native_balance_wei.return_value = ... # Ignored
|
|
338
|
+
mock_balance_service.get_native_balance_wei.return_value = 2000000000000000000
|
|
339
|
+
|
|
340
|
+
chain_interface.web3.eth.gas_price = 1000000000
|
|
341
|
+
chain_interface.web3.eth.estimate_gas.return_value = 21000
|
|
342
|
+
chain_interface.web3.from_wei.side_effect = lambda val, unit: float(val) / 10**18
|
|
343
|
+
|
|
344
|
+
# Mock return values for success
|
|
345
|
+
chain_interface.sign_and_send_transaction.return_value = (True, {})
|
|
346
|
+
chain_interface.send_native_transfer.return_value = (True, "0xHash")
|
|
347
|
+
|
|
348
|
+
wallet.send(
|
|
349
|
+
"sender", VALID_ADDR_2, amount_wei=1000000000000000000, token_address_or_name="native"
|
|
350
|
+
) # 1 ETH
|
|
351
|
+
|
|
352
|
+
# wallet.transaction_service.sign_and_send.assert_called_once()
|
|
353
|
+
chain_interface.send_native_transfer.assert_called_once()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_send_erc20_success(wallet, mock_key_storage, mock_chain_interfaces):
|
|
357
|
+
account = MagicMock(spec=StoredAccount)
|
|
358
|
+
account.address = VALID_ADDR_2
|
|
359
|
+
account.key = "private_key"
|
|
360
|
+
mock_key_storage.get_account.return_value = account
|
|
361
|
+
|
|
362
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
363
|
+
chain_interface.chain.tokens = {"TEST": "0xTokenAddress"}
|
|
364
|
+
chain_interface.web3.from_wei.side_effect = lambda val, unit: float(val) / 10**18
|
|
365
|
+
|
|
366
|
+
wallet.balance_service.get_erc20_balance_wei.return_value = 2000
|
|
367
|
+
wallet.balance_service.get_native_balance_wei.return_value = 1000000000000000000
|
|
368
|
+
|
|
369
|
+
# Mock TransactionService return
|
|
370
|
+
wallet.transaction_service.sign_and_send.return_value = (
|
|
371
|
+
True,
|
|
372
|
+
{"status": 1, "transactionHash": b"hash"},
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
with patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20:
|
|
376
|
+
with patch("iwa.core.services.transfer.erc20.ERC20Contract", new=mock_erc20):
|
|
377
|
+
erc20_instance = mock_erc20.return_value
|
|
378
|
+
erc20_instance.address = VALID_ADDR_1
|
|
379
|
+
erc20_instance.prepare_transfer_tx.return_value = {
|
|
380
|
+
"data": b"transfer_data",
|
|
381
|
+
"to": VALID_ADDR_1,
|
|
382
|
+
"value": 0,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
wallet.send("sender", "recipient", amount_wei=1000, token_address_or_name="TEST")
|
|
386
|
+
|
|
387
|
+
wallet.transaction_service.sign_and_send.assert_called_once()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_approve_erc20_success(wallet, mock_key_storage, mock_chain_interfaces):
|
|
391
|
+
account = MagicMock(spec=StoredAccount)
|
|
392
|
+
account.address = VALID_ADDR_2
|
|
393
|
+
account.key = "private_key"
|
|
394
|
+
mock_key_storage.get_account.return_value = account
|
|
395
|
+
|
|
396
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
397
|
+
chain_interface.chain.tokens = {"TEST": "0xTokenAddress"} # Needed for resolution
|
|
398
|
+
chain_interface.web3.from_wei.side_effect = lambda val, unit: float(val) / 10**18
|
|
399
|
+
|
|
400
|
+
wallet.transaction_service.sign_and_send.return_value = (
|
|
401
|
+
True,
|
|
402
|
+
{"status": 1, "transactionHash": b"hash"},
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
with patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20:
|
|
406
|
+
with patch("iwa.core.services.transfer.erc20.ERC20Contract", new=mock_erc20):
|
|
407
|
+
erc20_instance = mock_erc20.return_value
|
|
408
|
+
erc20_instance.allowance_wei.return_value = 0
|
|
409
|
+
erc20_instance.prepare_approve_tx.return_value = {
|
|
410
|
+
"data": b"approve_data",
|
|
411
|
+
"to": VALID_ADDR_1,
|
|
412
|
+
"value": 0,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
wallet.approve_erc20("owner", "spender", "TEST", 1000)
|
|
416
|
+
|
|
417
|
+
erc20_instance.prepare_approve_tx.assert_called_once()
|
|
418
|
+
wallet.transaction_service.sign_and_send.assert_called_once()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_approve_erc20_already_sufficient(wallet, mock_key_storage, mock_chain_interfaces):
|
|
422
|
+
account = MagicMock(spec=StoredAccount)
|
|
423
|
+
account.address = VALID_ADDR_2
|
|
424
|
+
mock_key_storage.get_account.return_value = account
|
|
425
|
+
|
|
426
|
+
with (
|
|
427
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
428
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract"),
|
|
429
|
+
):
|
|
430
|
+
erc20_instance = mock_erc20.return_value
|
|
431
|
+
erc20_instance.allowance_wei.return_value = 2000
|
|
432
|
+
|
|
433
|
+
wallet.approve_erc20("owner", "spender", "TEST", 1000)
|
|
434
|
+
|
|
435
|
+
erc20_instance.prepare_approve_tx.assert_not_called()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_multi_send_success(wallet, mock_key_storage, mock_chain_interfaces):
|
|
439
|
+
account = MagicMock(spec=StoredAccount)
|
|
440
|
+
account.address = VALID_ADDR_2
|
|
441
|
+
account.key = "private_key"
|
|
442
|
+
mock_key_storage.get_account.return_value = account
|
|
443
|
+
|
|
444
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
445
|
+
chain_interface.web3.to_wei.return_value = 1000
|
|
446
|
+
|
|
447
|
+
wallet.transaction_service.sign_and_send.return_value = (
|
|
448
|
+
True,
|
|
449
|
+
{"status": 1, "transactionHash": b"hash"},
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
with patch("iwa.core.services.transfer.multisend.MultiSendCallOnlyContract") as mock_multisend:
|
|
453
|
+
multisend_instance = mock_multisend.return_value
|
|
454
|
+
multisend_instance.prepare_tx.return_value = {
|
|
455
|
+
"data": b"multisend_data",
|
|
456
|
+
"to": "0xMultiSend",
|
|
457
|
+
"value": 0,
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
transactions = [
|
|
461
|
+
{
|
|
462
|
+
"to": VALID_ADDR_2,
|
|
463
|
+
"amount": 1.0,
|
|
464
|
+
"token": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
wallet.multi_send("sender", transactions)
|
|
469
|
+
|
|
470
|
+
multisend_instance.prepare_tx.assert_called_once()
|
|
471
|
+
wallet.transaction_service.sign_and_send.assert_called_once()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def test_drain_native_success(
|
|
475
|
+
wallet, mock_key_storage, mock_chain_interfaces, mock_balance_service
|
|
476
|
+
):
|
|
477
|
+
account = MagicMock(spec=StoredAccount)
|
|
478
|
+
account.address = VALID_ADDR_1
|
|
479
|
+
mock_key_storage.get_account.return_value = account
|
|
480
|
+
|
|
481
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
482
|
+
chain_interface.chain.tokens = {}
|
|
483
|
+
mock_balance_service.get_native_balance_wei.return_value = 2000000000000000000
|
|
484
|
+
chain_interface.web3.eth.gas_price = 1000000000
|
|
485
|
+
chain_interface.web3.from_wei.side_effect = lambda val, unit: float(val) / 10**18
|
|
486
|
+
chain_interface.web3.to_wei.side_effect = lambda val, unit: int(float(val) * 10**18)
|
|
487
|
+
|
|
488
|
+
# Mock return values
|
|
489
|
+
wallet.transaction_service.sign_and_send.return_value = (True, {"status": 1})
|
|
490
|
+
|
|
491
|
+
with patch("iwa.core.services.transfer.multisend.MultiSendCallOnlyContract") as mock_multisend:
|
|
492
|
+
multisend_instance = mock_multisend.return_value
|
|
493
|
+
multisend_instance.prepare_tx.return_value = {"to": "0x", "data": b"", "value": 0}
|
|
494
|
+
|
|
495
|
+
wallet.drain("sender", "recipient")
|
|
496
|
+
|
|
497
|
+
# Now drain uses multi_send
|
|
498
|
+
multisend_instance.prepare_tx.assert_called_once()
|
|
499
|
+
wallet.transaction_service.sign_and_send.assert_called_once()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def test_drain_erc20_success(wallet, mock_key_storage, mock_chain_interfaces):
|
|
503
|
+
account = MagicMock(spec=StoredAccount)
|
|
504
|
+
account.address = VALID_ADDR_2
|
|
505
|
+
mock_key_storage.get_account.return_value = account
|
|
506
|
+
|
|
507
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
508
|
+
chain_interface.chain.tokens = {"TEST": "0xTokenAddress"}
|
|
509
|
+
chain_interface.web3.from_wei.side_effect = lambda val, unit: float(val) / 10**18
|
|
510
|
+
chain_interface.web3.to_wei.side_effect = lambda val, unit: int(float(val) * 10**18)
|
|
511
|
+
|
|
512
|
+
wallet.balance_service.get_erc20_balance_wei.return_value = 1000
|
|
513
|
+
wallet.balance_service.get_native_balance_wei.return_value = 1000000000000000000
|
|
514
|
+
|
|
515
|
+
wallet.transaction_service.sign_and_send.return_value = (
|
|
516
|
+
True,
|
|
517
|
+
{"status": 1, "transactionHash": b"hash"},
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
with (
|
|
521
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
522
|
+
patch("iwa.core.services.transfer.multisend.MultiSendCallOnlyContract") as mock_multisend,
|
|
523
|
+
):
|
|
524
|
+
with patch("iwa.core.services.transfer.erc20.ERC20Contract", new=mock_erc20):
|
|
525
|
+
erc20_instance = mock_erc20.return_value
|
|
526
|
+
erc20_instance.prepare_transfer_tx.return_value = {"to": "0x", "data": b"", "value": 0}
|
|
527
|
+
|
|
528
|
+
multisend_instance = mock_multisend.return_value
|
|
529
|
+
multisend_instance.prepare_tx.return_value = {"to": "0x", "data": b"", "value": 0}
|
|
530
|
+
|
|
531
|
+
wallet.drain("sender", "recipient")
|
|
532
|
+
|
|
533
|
+
# Drain now uses multi_send batching
|
|
534
|
+
multisend_instance.prepare_tx.assert_called_once()
|
|
535
|
+
assert wallet.transaction_service.sign_and_send.call_count == 2
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@pytest.mark.asyncio
|
|
539
|
+
async def test_swap_success(wallet, mock_key_storage, mock_chain_interfaces, mock_cow_swap):
|
|
540
|
+
account = MagicMock(spec=StoredAccount)
|
|
541
|
+
account.address = VALID_ADDR_2
|
|
542
|
+
account.key = "private_key"
|
|
543
|
+
mock_key_storage.get_account.return_value = account
|
|
544
|
+
|
|
545
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
546
|
+
chain_interface.web3.to_wei.return_value = 1000
|
|
547
|
+
chain_interface.web3.from_wei.side_effect = lambda val, unit: float(val) / 10**18
|
|
548
|
+
|
|
549
|
+
cow_instance = mock_cow_swap.return_value
|
|
550
|
+
cow_instance.swap = MagicMock()
|
|
551
|
+
cow_instance.swap.return_value = True
|
|
552
|
+
|
|
553
|
+
# Make it awaitable
|
|
554
|
+
async def async_true(*args, **kwargs):
|
|
555
|
+
return True
|
|
556
|
+
|
|
557
|
+
cow_instance.swap.side_effect = async_true
|
|
558
|
+
|
|
559
|
+
with (
|
|
560
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
561
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract"),
|
|
562
|
+
):
|
|
563
|
+
erc20_instance = mock_erc20.return_value
|
|
564
|
+
erc20_instance.allowance_wei.return_value = 0
|
|
565
|
+
erc20_instance.prepare_approve_tx.return_value = {
|
|
566
|
+
"data": b"approve_data",
|
|
567
|
+
"to": VALID_ADDR_1,
|
|
568
|
+
"value": 0,
|
|
569
|
+
}
|
|
570
|
+
chain_interface.sign_and_send_transaction.return_value = (True, {})
|
|
571
|
+
|
|
572
|
+
# Mock balance for pre-swap validation
|
|
573
|
+
wallet.balance_service.get_erc20_balance_wei.return_value = (
|
|
574
|
+
2000000000000000000 # 2 ETH (> 1.0 ETH)
|
|
575
|
+
)
|
|
576
|
+
wallet.balance_service.get_native_balance_wei.return_value = 2000000000000000000 # 2 ETH
|
|
577
|
+
|
|
578
|
+
success = await wallet.swap("sender", 1.0, "SELL", "BUY")
|
|
579
|
+
|
|
580
|
+
assert success is True
|
|
581
|
+
cow_instance.swap.assert_called_once()
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def test_transfer_from_erc20_success(wallet, mock_key_storage, mock_chain_interfaces):
|
|
585
|
+
from_account = MagicMock(spec=StoredAccount)
|
|
586
|
+
from_account.address = VALID_ADDR_2
|
|
587
|
+
from_account.key = "private_key"
|
|
588
|
+
|
|
589
|
+
sender_account = MagicMock(spec=StoredAccount)
|
|
590
|
+
sender_account.address = VALID_ADDR_1
|
|
591
|
+
|
|
592
|
+
mock_key_storage.get_account.side_effect = (
|
|
593
|
+
lambda tag: from_account if tag == "from" else sender_account if tag == "sender" else None
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
597
|
+
chain_interface.chain.tokens = {"TEST": "0xTokenAddress"}
|
|
598
|
+
|
|
599
|
+
wallet.transaction_service.sign_and_send.return_value = (
|
|
600
|
+
True,
|
|
601
|
+
{"status": 1, "transactionHash": b"hash"},
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
with patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20:
|
|
605
|
+
with patch("iwa.core.services.transfer.erc20.ERC20Contract", new=mock_erc20):
|
|
606
|
+
erc20_instance = mock_erc20.return_value
|
|
607
|
+
erc20_instance.address = VALID_ADDR_1
|
|
608
|
+
erc20_instance.prepare_transfer_from_tx.return_value = {
|
|
609
|
+
"data": b"transfer_from_data",
|
|
610
|
+
"to": VALID_ADDR_1,
|
|
611
|
+
"value": 0,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
wallet.transfer_from_erc20("from", "sender", "recipient", "TEST", 1000)
|
|
615
|
+
|
|
616
|
+
wallet.transaction_service.sign_and_send.assert_called_once()
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def test_master_account(wallet, mock_account_service):
|
|
620
|
+
mock_account = MagicMock(spec=StoredSafeAccount)
|
|
621
|
+
mock_account_service.master_account = mock_account
|
|
622
|
+
assert wallet.master_account == mock_account
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def test_send_invalid_from_account(wallet, mock_key_storage):
|
|
626
|
+
mock_key_storage.get_account.return_value = None
|
|
627
|
+
wallet.send("unknown", "recipient", "native", 1000)
|
|
628
|
+
# Should log error and return
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def test_send_invalid_token(wallet, mock_key_storage, mock_chain_interfaces):
|
|
632
|
+
account = MagicMock(spec=StoredAccount)
|
|
633
|
+
account.address = "0xSender"
|
|
634
|
+
mock_key_storage.get_account.return_value = account
|
|
635
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
636
|
+
chain_interface.chain.get_token_address.return_value = None
|
|
637
|
+
|
|
638
|
+
wallet.send("sender", "recipient", "INVALID", 1000)
|
|
639
|
+
# Should log error and return
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def test_send_native_safe(wallet, mock_key_storage, mock_chain_interfaces, mock_balance_service):
|
|
643
|
+
account = StoredSafeAccount(
|
|
644
|
+
address=VALID_ADDR_1, tag="safe", chains=["gnosis"], signers=[VALID_ADDR_2], threshold=1
|
|
645
|
+
)
|
|
646
|
+
mock_key_storage.get_account.return_value = account
|
|
647
|
+
wallet.safe_service.execute_safe_transaction.return_value = "0xTxHash123"
|
|
648
|
+
|
|
649
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
650
|
+
chain_interface.web3.from_wei.return_value = 1.0
|
|
651
|
+
mock_balance_service.get_native_balance_wei.return_value = 2000000000000000000 # Enough balance
|
|
652
|
+
|
|
653
|
+
# NOTE: SafeMultisig is no longer used directly in TransferService.
|
|
654
|
+
# Safe transactions now go through SafeService.execute_safe_transaction().
|
|
655
|
+
# Let's just assert the delegation happened.
|
|
656
|
+
wallet.send("safe", "recipient", amount_wei=1000, token_address_or_name="native")
|
|
657
|
+
wallet.safe_service.execute_safe_transaction.assert_called_once()
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def test_send_erc20_safe(wallet, mock_key_storage, mock_chain_interfaces, mock_balance_service):
|
|
661
|
+
account = StoredSafeAccount(
|
|
662
|
+
address=VALID_ADDR_1, tag="safe", chains=["gnosis"], signers=[VALID_ADDR_2], threshold=1
|
|
663
|
+
)
|
|
664
|
+
mock_key_storage.get_account.return_value = account
|
|
665
|
+
wallet.safe_service.execute_safe_transaction.return_value = "0xTxHash123"
|
|
666
|
+
|
|
667
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
668
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
669
|
+
chain_interface.web3.from_wei.return_value = 1.0
|
|
670
|
+
|
|
671
|
+
# Needs balance for check
|
|
672
|
+
mock_balance_service.get_erc20_balance_wei.return_value = 2000
|
|
673
|
+
mock_balance_service.get_native_balance_wei.return_value = (
|
|
674
|
+
1000000000000000000 # Enough native for gas
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
with (
|
|
678
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
679
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract"),
|
|
680
|
+
):
|
|
681
|
+
erc20_instance = mock_erc20.return_value
|
|
682
|
+
erc20_instance.decimals = 18
|
|
683
|
+
erc20_instance.address = "0xToken"
|
|
684
|
+
erc20_instance.prepare_transfer_tx.return_value = {"data": b"data"}
|
|
685
|
+
|
|
686
|
+
wallet.send("safe", "recipient", amount_wei=1000, token_address_or_name="TEST")
|
|
687
|
+
wallet.safe_service.execute_safe_transaction.assert_called_once()
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def test_multi_send_invalid_from_account(wallet, mock_key_storage):
|
|
691
|
+
mock_key_storage.get_account.return_value = None
|
|
692
|
+
wallet.multi_send("unknown", [])
|
|
693
|
+
# Should log error and return
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def test_multi_send_erc20_eoa_success(wallet, mock_key_storage, mock_chain_interfaces):
|
|
697
|
+
account = MagicMock(spec=StoredAccount)
|
|
698
|
+
account.address = VALID_ADDR_2
|
|
699
|
+
mock_key_storage.get_account.return_value = account
|
|
700
|
+
|
|
701
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
702
|
+
chain_interface.web3.to_wei.side_effect = lambda val, unit: int(float(val) * 10**18)
|
|
703
|
+
chain_interface.chain.tokens = {"TEST": "0xTokenAddress"}
|
|
704
|
+
|
|
705
|
+
transactions = [{"to": "0xRecipient", "amount": 1.0, "token": "TEST"}]
|
|
706
|
+
|
|
707
|
+
wallet.transaction_service.sign_and_send.return_value = (True, {"status": 1})
|
|
708
|
+
|
|
709
|
+
with (
|
|
710
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
711
|
+
patch("iwa.core.services.transfer.multisend.MultiSendCallOnlyContract") as mock_multisend,
|
|
712
|
+
):
|
|
713
|
+
with patch("iwa.core.services.transfer.erc20.ERC20Contract", new=mock_erc20):
|
|
714
|
+
erc20_instance = mock_erc20.return_value
|
|
715
|
+
erc20_instance.prepare_transfer_from_tx.return_value = {
|
|
716
|
+
"to": "0x",
|
|
717
|
+
"data": b"",
|
|
718
|
+
"value": 0,
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
multisend_instance = mock_multisend.return_value
|
|
722
|
+
multisend_instance.prepare_tx.return_value = {"to": "0x", "data": b"", "value": 0}
|
|
723
|
+
|
|
724
|
+
wallet.multi_send("sender", transactions)
|
|
725
|
+
|
|
726
|
+
# Now EOA supports MultiSend with ERC20 (requires approval first)
|
|
727
|
+
multisend_instance.prepare_tx.assert_called_once()
|
|
728
|
+
assert wallet.transaction_service.sign_and_send.call_count == 2
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def test_multi_send_safe(wallet, mock_key_storage, mock_chain_interfaces):
|
|
732
|
+
account = StoredSafeAccount(
|
|
733
|
+
address=VALID_ADDR_1, tag="safe", chains=["gnosis"], signers=[VALID_ADDR_2], threshold=1
|
|
734
|
+
)
|
|
735
|
+
mock_key_storage.get_account.return_value = account
|
|
736
|
+
wallet.safe_service.execute_safe_transaction.return_value = "0xTxHash123"
|
|
737
|
+
|
|
738
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
739
|
+
chain_interface.web3.to_wei.return_value = 1000
|
|
740
|
+
|
|
741
|
+
with patch("iwa.core.services.transfer.multisend.MultiSendContract") as mock_multisend:
|
|
742
|
+
multisend_instance = mock_multisend.return_value
|
|
743
|
+
multisend_instance.prepare_tx.return_value = {
|
|
744
|
+
"data": b"multisend_data",
|
|
745
|
+
"to": "0xMultiSend",
|
|
746
|
+
"value": 0,
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
transactions = [
|
|
750
|
+
{
|
|
751
|
+
"to": VALID_ADDR_2,
|
|
752
|
+
"amount": 1.0,
|
|
753
|
+
"token": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
|
754
|
+
}
|
|
755
|
+
]
|
|
756
|
+
wallet.multi_send("safe", transactions)
|
|
757
|
+
|
|
758
|
+
wallet.safe_service.execute_safe_transaction.assert_called_once()
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def test_get_erc20_balance_eth_success(
|
|
762
|
+
wallet, mock_key_storage, mock_chain_interfaces, mock_balance_service
|
|
763
|
+
):
|
|
764
|
+
account = MagicMock(spec=StoredAccount)
|
|
765
|
+
account.address = "0xAccount"
|
|
766
|
+
mock_key_storage.get_account.return_value = account
|
|
767
|
+
|
|
768
|
+
# Chain interface setup no longer strictly needed for balance service mock but helps consistency
|
|
769
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
770
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
771
|
+
|
|
772
|
+
mock_balance_service.get_erc20_balance_eth.return_value = 10.0
|
|
773
|
+
balance = wallet.get_erc20_balance_eth("account", "TEST")
|
|
774
|
+
assert balance == 10.0
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def test_get_erc20_balance_eth_token_not_found(wallet, mock_balance_service):
|
|
778
|
+
mock_balance_service.get_erc20_balance_eth.return_value = None
|
|
779
|
+
balance = wallet.get_erc20_balance_eth("account", "INVALID")
|
|
780
|
+
assert balance is None
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def test_get_erc20_balance_eth_account_not_found(wallet, mock_balance_service):
|
|
784
|
+
mock_balance_service.get_erc20_balance_eth.return_value = None
|
|
785
|
+
balance = wallet.get_erc20_balance_eth("account", "TEST")
|
|
786
|
+
assert balance is None
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def test_get_erc20_balance_wei_token_not_found(wallet, mock_balance_service):
|
|
790
|
+
mock_balance_service.get_erc20_balance_wei.return_value = None
|
|
791
|
+
balance = wallet.get_erc20_balance_wei("account", "INVALID")
|
|
792
|
+
assert balance is None
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def test_get_erc20_balance_wei_account_not_found(wallet, mock_balance_service):
|
|
796
|
+
mock_balance_service.get_erc20_balance_wei.return_value = None
|
|
797
|
+
balance = wallet.get_erc20_balance_wei("account", "TEST")
|
|
798
|
+
assert balance is None
|
|
799
|
+
|
|
800
|
+
balance = wallet.get_erc20_balance_wei("unknown", "TEST")
|
|
801
|
+
assert balance is None
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def test_get_erc20_allowance_token_not_found(wallet, mock_chain_interfaces):
|
|
805
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
806
|
+
chain_interface.chain.get_token_address.return_value = None
|
|
807
|
+
|
|
808
|
+
allowance = wallet.get_erc20_allowance("owner", "spender", "INVALID")
|
|
809
|
+
assert allowance is None
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def test_get_erc20_allowance_owner_not_found(wallet, mock_key_storage, mock_chain_interfaces):
|
|
813
|
+
mock_key_storage.get_account.return_value = None
|
|
814
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
815
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
816
|
+
|
|
817
|
+
allowance = wallet.get_erc20_allowance("unknown", "spender", "TEST")
|
|
818
|
+
assert allowance is None
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def test_approve_erc20_owner_not_found(wallet, mock_key_storage):
|
|
822
|
+
mock_key_storage.get_account.return_value = None
|
|
823
|
+
wallet.approve_erc20("unknown", "spender", "TEST", 1000)
|
|
824
|
+
# Should log error and return
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def test_approve_erc20_token_not_found(wallet, mock_key_storage, mock_chain_interfaces):
|
|
828
|
+
account = MagicMock(spec=StoredAccount)
|
|
829
|
+
account.address = "0xAccount"
|
|
830
|
+
mock_key_storage.get_account.return_value = account
|
|
831
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
832
|
+
chain_interface.get_token_address.return_value = None
|
|
833
|
+
|
|
834
|
+
wallet.approve_erc20("owner", "spender", "INVALID", 1000)
|
|
835
|
+
# Should return
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def test_approve_erc20_tx_prep_failed(wallet, mock_key_storage, mock_chain_interfaces):
|
|
839
|
+
account = MagicMock(spec=StoredAccount)
|
|
840
|
+
account.address = VALID_ADDR_2
|
|
841
|
+
mock_key_storage.get_account.return_value = account
|
|
842
|
+
|
|
843
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
844
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
845
|
+
|
|
846
|
+
with (
|
|
847
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
848
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract"),
|
|
849
|
+
):
|
|
850
|
+
erc20_instance = mock_erc20.return_value
|
|
851
|
+
erc20_instance.allowance_wei.return_value = 0
|
|
852
|
+
erc20_instance.prepare_approve_tx.return_value = None
|
|
853
|
+
|
|
854
|
+
wallet.approve_erc20("owner", "spender", "TEST", 1000)
|
|
855
|
+
# Should return
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def test_approve_erc20_safe(wallet, mock_key_storage, mock_chain_interfaces):
|
|
859
|
+
account = StoredSafeAccount(
|
|
860
|
+
address=VALID_ADDR_1, tag="safe", chains=["gnosis"], signers=[VALID_ADDR_2], threshold=1
|
|
861
|
+
)
|
|
862
|
+
mock_key_storage.get_account.return_value = account
|
|
863
|
+
wallet.safe_service.execute_safe_transaction.return_value = "0xTxHash123"
|
|
864
|
+
|
|
865
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
866
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
867
|
+
chain_interface.web3.from_wei.return_value = 1.0
|
|
868
|
+
|
|
869
|
+
with (
|
|
870
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
871
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract"),
|
|
872
|
+
):
|
|
873
|
+
erc20_instance = mock_erc20.return_value
|
|
874
|
+
erc20_instance.allowance_wei.return_value = 0
|
|
875
|
+
erc20_instance.prepare_approve_tx.return_value = {"data": b"data"}
|
|
876
|
+
|
|
877
|
+
wallet.approve_erc20("safe", "spender", "TEST", 1000)
|
|
878
|
+
wallet.safe_service.execute_safe_transaction.assert_called_once()
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def test_transfer_from_erc20_sender_not_found(wallet, mock_key_storage):
|
|
882
|
+
# from_account found, sender not found
|
|
883
|
+
from_account = MagicMock(spec=StoredAccount)
|
|
884
|
+
mock_key_storage.get_account.side_effect = lambda tag: from_account if tag == "from" else None
|
|
885
|
+
|
|
886
|
+
wallet.transfer_from_erc20("from", "unknown", "recipient", "TEST", 1000)
|
|
887
|
+
# Should log error and return
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def test_transfer_from_erc20_token_not_found(wallet, mock_key_storage, mock_chain_interfaces):
|
|
891
|
+
account = MagicMock(spec=StoredAccount)
|
|
892
|
+
account.address = "0xAccount"
|
|
893
|
+
mock_key_storage.get_account.return_value = account
|
|
894
|
+
|
|
895
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
896
|
+
chain_interface.get_token_address.return_value = None
|
|
897
|
+
|
|
898
|
+
wallet.transfer_from_erc20("from", "sender", "recipient", "INVALID", 1000)
|
|
899
|
+
# Should return
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def test_transfer_from_erc20_tx_prep_failed(wallet, mock_key_storage, mock_chain_interfaces):
|
|
903
|
+
account = MagicMock(spec=StoredAccount)
|
|
904
|
+
account.address = VALID_ADDR_2
|
|
905
|
+
mock_key_storage.get_account.return_value = account
|
|
906
|
+
|
|
907
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
908
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
909
|
+
|
|
910
|
+
with (
|
|
911
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
912
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract"),
|
|
913
|
+
):
|
|
914
|
+
erc20_instance = mock_erc20.return_value
|
|
915
|
+
erc20_instance.prepare_transfer_from_tx.return_value = None
|
|
916
|
+
|
|
917
|
+
wallet.transfer_from_erc20("from", "sender", "recipient", "TEST", 1000)
|
|
918
|
+
# Should return
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def test_transfer_from_erc20_safe(wallet, mock_key_storage, mock_chain_interfaces):
|
|
922
|
+
from_account = MagicMock(spec=StoredSafeAccount)
|
|
923
|
+
from_account.address = VALID_ADDR_1
|
|
924
|
+
sender_account = MagicMock(spec=StoredAccount)
|
|
925
|
+
sender_account.address = VALID_ADDR_2
|
|
926
|
+
|
|
927
|
+
# Needs chains for Safe
|
|
928
|
+
from_account.chains = ["gnosis"]
|
|
929
|
+
from_account.threshold = 1 # Ensure is_safe=True
|
|
930
|
+
|
|
931
|
+
mock_key_storage.get_account.side_effect = (
|
|
932
|
+
lambda tag: from_account if tag == "safe" else sender_account
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
wallet.safe_service.execute_safe_transaction.return_value = "0xTxHash123"
|
|
936
|
+
|
|
937
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
938
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
939
|
+
|
|
940
|
+
with (
|
|
941
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
942
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract"),
|
|
943
|
+
):
|
|
944
|
+
erc20_instance = mock_erc20.return_value
|
|
945
|
+
erc20_instance.prepare_transfer_from_tx.return_value = {"data": b"data"}
|
|
946
|
+
erc20_instance.address = "0xToken"
|
|
947
|
+
|
|
948
|
+
wallet.transfer_from_erc20("safe", "sender", "recipient", "TEST", 1000)
|
|
949
|
+
wallet.safe_service.execute_safe_transaction.assert_called_once()
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
@pytest.mark.asyncio
|
|
953
|
+
async def test_swap_buy_no_amount(wallet):
|
|
954
|
+
with pytest.raises(ValueError, match="Amount must be specified for buy orders"):
|
|
955
|
+
await wallet.swap("account", None, "SELL", "BUY", order_type=OrderType.BUY)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
@pytest.mark.asyncio
|
|
959
|
+
async def test_swap_max_retries(wallet, mock_key_storage, mock_chain_interfaces, mock_cow_swap):
|
|
960
|
+
account = MagicMock(spec=StoredAccount)
|
|
961
|
+
account.address = VALID_ADDR_2
|
|
962
|
+
account.key = "private_key"
|
|
963
|
+
mock_key_storage.get_account.return_value = account
|
|
964
|
+
|
|
965
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
966
|
+
chain_interface.web3.to_wei.return_value = 1000
|
|
967
|
+
chain_interface.web3.from_wei.side_effect = lambda val, unit: float(val) / 10**18
|
|
968
|
+
chain_interface.sign_and_send_transaction.return_value = (True, {})
|
|
969
|
+
|
|
970
|
+
cow_instance = mock_cow_swap.return_value
|
|
971
|
+
cow_instance.get_max_sell_amount_wei = AsyncMock(return_value=1000)
|
|
972
|
+
cow_instance.swap = AsyncMock(return_value=False) # Always fail
|
|
973
|
+
|
|
974
|
+
cow_instance.get_max_sell_amount_wei = AsyncMock(return_value=1000)
|
|
975
|
+
cow_instance.swap = AsyncMock(return_value=False) # Always fail
|
|
976
|
+
|
|
977
|
+
with patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20:
|
|
978
|
+
mock_erc20.return_value.allowance_wei.return_value = 0
|
|
979
|
+
|
|
980
|
+
# Mock balance
|
|
981
|
+
wallet.balance_service.get_erc20_balance_wei.return_value = 2000000000000000000
|
|
982
|
+
wallet.balance_service.get_native_balance_wei.return_value = 2000000000000000000
|
|
983
|
+
|
|
984
|
+
await wallet.swap("account", 1.0, "SELL", "BUY")
|
|
985
|
+
# Should log error after retries
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def test_drain_from_account_not_found(wallet, mock_key_storage, mock_account_service):
|
|
989
|
+
mock_account_service.resolve_account.return_value = None
|
|
990
|
+
wallet.drain("unknown")
|
|
991
|
+
# Should log error and return
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def test_drain_no_token_balance(
|
|
995
|
+
wallet, mock_key_storage, mock_chain_interfaces, mock_account_service, mock_balance_service
|
|
996
|
+
):
|
|
997
|
+
account = MagicMock(spec=StoredAccount)
|
|
998
|
+
account.address = VALID_ADDR_1
|
|
999
|
+
mock_account_service.resolve_account.return_value = account
|
|
1000
|
+
|
|
1001
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
1002
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
1003
|
+
mock_balance_service.get_native_balance_wei.return_value = 0
|
|
1004
|
+
mock_balance_service.get_erc20_balance_wei.return_value = 0
|
|
1005
|
+
|
|
1006
|
+
wallet.drain("account")
|
|
1007
|
+
# Should log info and continue
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def test_drain_native_safe(wallet, mock_key_storage, mock_chain_interfaces, mock_balance_service):
|
|
1011
|
+
account = StoredSafeAccount(
|
|
1012
|
+
address=VALID_ADDR_1, tag="safe", chains=["gnosis"], signers=[VALID_ADDR_2], threshold=1
|
|
1013
|
+
)
|
|
1014
|
+
mock_key_storage.get_account.return_value = account
|
|
1015
|
+
wallet.safe_service.execute_safe_transaction.return_value = "0xTxHash123"
|
|
1016
|
+
|
|
1017
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
1018
|
+
chain_interface.chain.tokens = {}
|
|
1019
|
+
mock_balance_service.get_native_balance_wei.return_value = 2000000000000000000
|
|
1020
|
+
chain_interface.web3.from_wei.return_value = 2.0
|
|
1021
|
+
|
|
1022
|
+
with patch("iwa.core.services.transfer.multisend.MultiSendContract") as mock_multisend:
|
|
1023
|
+
mock_multisend.return_value.prepare_tx.return_value = {
|
|
1024
|
+
"to": "0xMultiSend",
|
|
1025
|
+
"data": b"multisend_data",
|
|
1026
|
+
"value": 0,
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
wallet.drain("safe")
|
|
1030
|
+
wallet.safe_service.execute_safe_transaction.assert_called_once()
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def test_drain_not_enough_native_balance(
|
|
1034
|
+
wallet, mock_key_storage, mock_chain_interfaces, mock_account_service, mock_balance_service
|
|
1035
|
+
):
|
|
1036
|
+
account = MagicMock(spec=StoredAccount)
|
|
1037
|
+
account.address = VALID_ADDR_1
|
|
1038
|
+
mock_account_service.resolve_account.return_value = account
|
|
1039
|
+
|
|
1040
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
1041
|
+
chain_interface.chain.tokens = {}
|
|
1042
|
+
mock_balance_service.get_native_balance_wei.return_value = 1000 # Very low balance
|
|
1043
|
+
chain_interface.web3.eth.gas_price = 1000000000
|
|
1044
|
+
|
|
1045
|
+
wallet.drain("account")
|
|
1046
|
+
# Should log info and return
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def test_send_erc20_tx_prep_failed(wallet, mock_key_storage, mock_chain_interfaces):
|
|
1050
|
+
account = MagicMock(spec=StoredAccount)
|
|
1051
|
+
account.address = VALID_ADDR_2
|
|
1052
|
+
mock_key_storage.get_account.return_value = account
|
|
1053
|
+
|
|
1054
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
1055
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
1056
|
+
|
|
1057
|
+
with (
|
|
1058
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
1059
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract"),
|
|
1060
|
+
):
|
|
1061
|
+
erc20_instance = mock_erc20.return_value
|
|
1062
|
+
erc20_instance.prepare_transfer_tx.return_value = None
|
|
1063
|
+
|
|
1064
|
+
wallet.send("sender", "recipient", "TEST", 1000)
|
|
1065
|
+
# Should return
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def test_multi_send_tx_prep_failed(wallet, mock_key_storage, mock_chain_interfaces):
|
|
1069
|
+
account = StoredSafeAccount(
|
|
1070
|
+
address=VALID_ADDR_1, tag="safe", chains=["gnosis"], signers=[VALID_ADDR_2], threshold=1
|
|
1071
|
+
)
|
|
1072
|
+
mock_key_storage.get_account.return_value = account
|
|
1073
|
+
|
|
1074
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
1075
|
+
chain_interface.web3.to_wei.return_value = 1000
|
|
1076
|
+
|
|
1077
|
+
with patch("iwa.core.services.transfer.multisend.MultiSendContract") as mock_multisend:
|
|
1078
|
+
multisend_instance = mock_multisend.return_value
|
|
1079
|
+
multisend_instance.prepare_tx.return_value = None
|
|
1080
|
+
|
|
1081
|
+
transactions = [
|
|
1082
|
+
{
|
|
1083
|
+
"to": VALID_ADDR_2,
|
|
1084
|
+
"amount": 1.0,
|
|
1085
|
+
"token": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
|
1086
|
+
}
|
|
1087
|
+
]
|
|
1088
|
+
wallet.multi_send("safe", transactions)
|
|
1089
|
+
# Should return
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
@pytest.mark.asyncio
|
|
1093
|
+
async def test_swap_entire_balance(wallet, mock_key_storage, mock_chain_interfaces, mock_cow_swap):
|
|
1094
|
+
account = MagicMock(spec=StoredAccount)
|
|
1095
|
+
account.address = VALID_ADDR_2
|
|
1096
|
+
mock_key_storage.get_account.return_value = account
|
|
1097
|
+
|
|
1098
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
1099
|
+
sell_token_address = VALID_ADDR_1
|
|
1100
|
+
chain_interface.chain.tokens = {"SELL": sell_token_address}
|
|
1101
|
+
chain_interface.get_token_address.return_value = sell_token_address
|
|
1102
|
+
chain_interface.chain.get_token_address.return_value = sell_token_address
|
|
1103
|
+
chain_interface.get_erc20_allowance.return_value = 0 # Added default allowance
|
|
1104
|
+
|
|
1105
|
+
cow_instance = mock_cow_swap.return_value
|
|
1106
|
+
cow_instance.swap = AsyncMock(return_value=True)
|
|
1107
|
+
|
|
1108
|
+
# Create a shared mock for both
|
|
1109
|
+
erc20_mock = MagicMock()
|
|
1110
|
+
erc20_instance = erc20_mock.return_value
|
|
1111
|
+
erc20_instance.balance_of_wei.return_value = 1000
|
|
1112
|
+
erc20_instance.allowance_wei.return_value = 0
|
|
1113
|
+
erc20_instance.prepare_approve_tx.return_value = {
|
|
1114
|
+
"data": b"approve_data",
|
|
1115
|
+
"to": VALID_ADDR_1,
|
|
1116
|
+
"value": 0,
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
with (
|
|
1120
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract", new=erc20_mock),
|
|
1121
|
+
patch("iwa.core.services.balance.ERC20Contract", new=erc20_mock),
|
|
1122
|
+
):
|
|
1123
|
+
await wallet.swap("account", None, "SELL", "BUY")
|
|
1124
|
+
|
|
1125
|
+
cow_instance.swap.assert_called_once()
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def test_multi_send_erc20_safe_success(wallet, mock_key_storage, mock_chain_interfaces):
|
|
1129
|
+
account = StoredSafeAccount(
|
|
1130
|
+
address=VALID_ADDR_1, tag="safe", chains=["gnosis"], signers=[VALID_ADDR_2], threshold=1
|
|
1131
|
+
)
|
|
1132
|
+
mock_key_storage.get_account.return_value = account
|
|
1133
|
+
wallet.safe_service.execute_safe_transaction.return_value = "0xTxHash123"
|
|
1134
|
+
|
|
1135
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
1136
|
+
chain_interface.web3.to_wei.return_value = 1000
|
|
1137
|
+
chain_interface.chain.tokens = {"TEST": "0xToken"}
|
|
1138
|
+
|
|
1139
|
+
with (
|
|
1140
|
+
patch("iwa.core.services.transfer.multisend.MultiSendContract") as mock_multisend,
|
|
1141
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
1142
|
+
):
|
|
1143
|
+
multisend_instance = mock_multisend.return_value
|
|
1144
|
+
multisend_instance.prepare_tx.return_value = {
|
|
1145
|
+
"data": b"multisend_data",
|
|
1146
|
+
"to": "0xMultiSend",
|
|
1147
|
+
"value": 0,
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
erc20_instance = mock_erc20.return_value
|
|
1151
|
+
erc20_instance.decimals = 18
|
|
1152
|
+
erc20_instance.address = "0xToken"
|
|
1153
|
+
erc20_instance.prepare_transfer_tx.return_value = {"data": b"transfer_data"}
|
|
1154
|
+
|
|
1155
|
+
transactions = [{"to": VALID_ADDR_2, "amount": 1.0, "token": "TEST"}]
|
|
1156
|
+
wallet.multi_send("safe", transactions)
|
|
1157
|
+
|
|
1158
|
+
wallet.safe_service.execute_safe_transaction.assert_called_once()
|
|
1159
|
+
wallet.transaction_service.sign_and_send.assert_not_called()
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
# --- Negative Tests for TransferService ---
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def test_send_whitelist_rejected(wallet, mock_key_storage, mock_chain_interfaces):
|
|
1166
|
+
"""Test send fails when destination not in whitelist."""
|
|
1167
|
+
# Override the auto-mock to test actual security validation
|
|
1168
|
+
with patch(
|
|
1169
|
+
"iwa.core.services.transfer.TransferService._is_whitelisted_destination",
|
|
1170
|
+
return_value=False, # Simulate rejected destination
|
|
1171
|
+
):
|
|
1172
|
+
mock_key_storage.get_account.return_value = MagicMock(
|
|
1173
|
+
address=VALID_ADDR_1,
|
|
1174
|
+
tag="sender",
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
result = wallet.send(
|
|
1178
|
+
from_address_or_tag=VALID_ADDR_1,
|
|
1179
|
+
to_address_or_tag=VALID_ADDR_2, # Not in whitelist
|
|
1180
|
+
token_address_or_name="native",
|
|
1181
|
+
amount_wei=10**18,
|
|
1182
|
+
chain_name="gnosis",
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
assert result is None # Should fail due to whitelist
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def test_send_unsupported_token_rejected(wallet, mock_key_storage, mock_chain_interfaces):
|
|
1189
|
+
"""Test send fails when token is not supported."""
|
|
1190
|
+
# Override the auto-mock to test actual security validation
|
|
1191
|
+
with patch(
|
|
1192
|
+
"iwa.core.services.transfer.TransferService._is_supported_token",
|
|
1193
|
+
return_value=False, # Simulate unsupported token
|
|
1194
|
+
):
|
|
1195
|
+
mock_key_storage.get_account.return_value = MagicMock(
|
|
1196
|
+
address=VALID_ADDR_1,
|
|
1197
|
+
tag="sender",
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
result = wallet.send(
|
|
1201
|
+
from_address_or_tag=VALID_ADDR_1,
|
|
1202
|
+
to_address_or_tag=VALID_ADDR_2,
|
|
1203
|
+
token_address_or_name="UNKNOWN_TOKEN", # Not supported
|
|
1204
|
+
amount_wei=10**18,
|
|
1205
|
+
chain_name="gnosis",
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
assert result is None # Should fail due to unsupported token
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def test_send_zero_amount(wallet, mock_key_storage, mock_chain_interfaces):
|
|
1212
|
+
"""Test send with zero amount."""
|
|
1213
|
+
mock_key_storage.get_account.return_value = MagicMock(
|
|
1214
|
+
address=VALID_ADDR_1,
|
|
1215
|
+
tag="sender",
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
1219
|
+
chain_interface.web3.from_wei.return_value = 0.0
|
|
1220
|
+
chain_interface.send_native_transfer.return_value = (True, "0xHash")
|
|
1221
|
+
|
|
1222
|
+
wallet.send(
|
|
1223
|
+
from_address_or_tag=VALID_ADDR_1,
|
|
1224
|
+
to_address_or_tag=VALID_ADDR_2,
|
|
1225
|
+
token_address_or_name="native",
|
|
1226
|
+
amount_wei=0,
|
|
1227
|
+
chain_name="gnosis",
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
# Zero amount should be handled (may succeed or fail gracefully)
|
|
1231
|
+
# At minimum, should not crash
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def test_send_same_source_destination(
|
|
1235
|
+
wallet, mock_key_storage, mock_balance_service, mock_chain_interfaces
|
|
1236
|
+
):
|
|
1237
|
+
"""Test send when source equals destination."""
|
|
1238
|
+
mock_key_storage.get_account.return_value = MagicMock(
|
|
1239
|
+
address=VALID_ADDR_1,
|
|
1240
|
+
tag="sender",
|
|
1241
|
+
)
|
|
1242
|
+
mock_balance_service.get_native_balance_wei.return_value = 10**19
|
|
1243
|
+
|
|
1244
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
1245
|
+
chain_interface.web3.from_wei.return_value = 1.0
|
|
1246
|
+
chain_interface.send_native_transfer.return_value = (True, "0xHash")
|
|
1247
|
+
|
|
1248
|
+
# Self-transfer should work but is unusual
|
|
1249
|
+
wallet.send(
|
|
1250
|
+
from_address_or_tag=VALID_ADDR_1,
|
|
1251
|
+
to_address_or_tag=VALID_ADDR_1, # Same as source
|
|
1252
|
+
token_address_or_name="native",
|
|
1253
|
+
amount_wei=10**18,
|
|
1254
|
+
chain_name="gnosis",
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
# Should not crash
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def test_send_account_not_found(wallet, mock_key_storage):
|
|
1261
|
+
"""Test send fails when from account doesn't exist."""
|
|
1262
|
+
mock_key_storage.get_account.return_value = None
|
|
1263
|
+
|
|
1264
|
+
result = wallet.send(
|
|
1265
|
+
from_address_or_tag="nonexistent_account",
|
|
1266
|
+
to_address_or_tag=VALID_ADDR_2,
|
|
1267
|
+
token_address_or_name="native",
|
|
1268
|
+
amount_wei=10**18,
|
|
1269
|
+
chain_name="gnosis",
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
assert result is None
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def test_multi_send_empty_transactions(wallet, mock_key_storage):
|
|
1276
|
+
"""Test multi_send with empty transaction list."""
|
|
1277
|
+
mock_key_storage.get_account.return_value = MagicMock(
|
|
1278
|
+
address=VALID_ADDR_1,
|
|
1279
|
+
tag="sender",
|
|
1280
|
+
)
|
|
1281
|
+
|
|
1282
|
+
# Empty list should be handled gracefully
|
|
1283
|
+
wallet.multi_send(VALID_ADDR_1, [])
|
|
1284
|
+
|
|
1285
|
+
# Should not crash
|