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.
Files changed (191) hide show
  1. conftest.py +22 -0
  2. iwa/__init__.py +1 -0
  3. iwa/__main__.py +6 -0
  4. iwa/core/__init__.py +1 -0
  5. iwa/core/chain/__init__.py +68 -0
  6. iwa/core/chain/errors.py +47 -0
  7. iwa/core/chain/interface.py +514 -0
  8. iwa/core/chain/manager.py +38 -0
  9. iwa/core/chain/models.py +128 -0
  10. iwa/core/chain/rate_limiter.py +193 -0
  11. iwa/core/cli.py +210 -0
  12. iwa/core/constants.py +28 -0
  13. iwa/core/contracts/__init__.py +1 -0
  14. iwa/core/contracts/contract.py +297 -0
  15. iwa/core/contracts/erc20.py +79 -0
  16. iwa/core/contracts/multisend.py +71 -0
  17. iwa/core/db.py +317 -0
  18. iwa/core/keys.py +361 -0
  19. iwa/core/mnemonic.py +385 -0
  20. iwa/core/models.py +344 -0
  21. iwa/core/monitor.py +209 -0
  22. iwa/core/plugins.py +45 -0
  23. iwa/core/pricing.py +91 -0
  24. iwa/core/services/__init__.py +17 -0
  25. iwa/core/services/account.py +57 -0
  26. iwa/core/services/balance.py +113 -0
  27. iwa/core/services/plugin.py +88 -0
  28. iwa/core/services/safe.py +392 -0
  29. iwa/core/services/transaction.py +172 -0
  30. iwa/core/services/transfer/__init__.py +166 -0
  31. iwa/core/services/transfer/base.py +260 -0
  32. iwa/core/services/transfer/erc20.py +247 -0
  33. iwa/core/services/transfer/multisend.py +386 -0
  34. iwa/core/services/transfer/native.py +262 -0
  35. iwa/core/services/transfer/swap.py +326 -0
  36. iwa/core/settings.py +95 -0
  37. iwa/core/tables.py +60 -0
  38. iwa/core/test.py +27 -0
  39. iwa/core/tests/test_wallet.py +255 -0
  40. iwa/core/types.py +59 -0
  41. iwa/core/ui.py +99 -0
  42. iwa/core/utils.py +59 -0
  43. iwa/core/wallet.py +380 -0
  44. iwa/plugins/__init__.py +1 -0
  45. iwa/plugins/gnosis/__init__.py +5 -0
  46. iwa/plugins/gnosis/cow/__init__.py +6 -0
  47. iwa/plugins/gnosis/cow/quotes.py +148 -0
  48. iwa/plugins/gnosis/cow/swap.py +403 -0
  49. iwa/plugins/gnosis/cow/types.py +20 -0
  50. iwa/plugins/gnosis/cow_utils.py +44 -0
  51. iwa/plugins/gnosis/plugin.py +68 -0
  52. iwa/plugins/gnosis/safe.py +157 -0
  53. iwa/plugins/gnosis/tests/test_cow.py +227 -0
  54. iwa/plugins/gnosis/tests/test_safe.py +100 -0
  55. iwa/plugins/olas/__init__.py +5 -0
  56. iwa/plugins/olas/constants.py +106 -0
  57. iwa/plugins/olas/contracts/activity_checker.py +93 -0
  58. iwa/plugins/olas/contracts/base.py +10 -0
  59. iwa/plugins/olas/contracts/mech.py +49 -0
  60. iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
  61. iwa/plugins/olas/contracts/service.py +215 -0
  62. iwa/plugins/olas/contracts/staking.py +403 -0
  63. iwa/plugins/olas/importer.py +736 -0
  64. iwa/plugins/olas/mech_reference.py +135 -0
  65. iwa/plugins/olas/models.py +110 -0
  66. iwa/plugins/olas/plugin.py +243 -0
  67. iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
  68. iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
  69. iwa/plugins/olas/service_manager/__init__.py +60 -0
  70. iwa/plugins/olas/service_manager/base.py +113 -0
  71. iwa/plugins/olas/service_manager/drain.py +336 -0
  72. iwa/plugins/olas/service_manager/lifecycle.py +839 -0
  73. iwa/plugins/olas/service_manager/mech.py +322 -0
  74. iwa/plugins/olas/service_manager/staking.py +530 -0
  75. iwa/plugins/olas/tests/conftest.py +30 -0
  76. iwa/plugins/olas/tests/test_importer.py +128 -0
  77. iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
  78. iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
  79. iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
  80. iwa/plugins/olas/tests/test_olas_integration.py +561 -0
  81. iwa/plugins/olas/tests/test_olas_models.py +144 -0
  82. iwa/plugins/olas/tests/test_olas_view.py +258 -0
  83. iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
  84. iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
  85. iwa/plugins/olas/tests/test_plugin.py +70 -0
  86. iwa/plugins/olas/tests/test_plugin_full.py +212 -0
  87. iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
  88. iwa/plugins/olas/tests/test_service_manager.py +1065 -0
  89. iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
  90. iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
  91. iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
  92. iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
  93. iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
  94. iwa/plugins/olas/tests/test_service_staking.py +342 -0
  95. iwa/plugins/olas/tests/test_staking_integration.py +269 -0
  96. iwa/plugins/olas/tests/test_staking_validation.py +109 -0
  97. iwa/plugins/olas/tui/__init__.py +1 -0
  98. iwa/plugins/olas/tui/olas_view.py +952 -0
  99. iwa/tools/check_profile.py +67 -0
  100. iwa/tools/release.py +111 -0
  101. iwa/tools/reset_env.py +111 -0
  102. iwa/tools/reset_tenderly.py +362 -0
  103. iwa/tools/restore_backup.py +82 -0
  104. iwa/tui/__init__.py +1 -0
  105. iwa/tui/app.py +174 -0
  106. iwa/tui/modals/__init__.py +5 -0
  107. iwa/tui/modals/base.py +406 -0
  108. iwa/tui/rpc.py +63 -0
  109. iwa/tui/screens/__init__.py +1 -0
  110. iwa/tui/screens/wallets.py +749 -0
  111. iwa/tui/tests/test_app.py +125 -0
  112. iwa/tui/tests/test_rpc.py +139 -0
  113. iwa/tui/tests/test_wallets_refactor.py +30 -0
  114. iwa/tui/tests/test_widgets.py +123 -0
  115. iwa/tui/widgets/__init__.py +5 -0
  116. iwa/tui/widgets/base.py +100 -0
  117. iwa/tui/workers.py +42 -0
  118. iwa/web/dependencies.py +76 -0
  119. iwa/web/models.py +76 -0
  120. iwa/web/routers/accounts.py +115 -0
  121. iwa/web/routers/olas/__init__.py +24 -0
  122. iwa/web/routers/olas/admin.py +169 -0
  123. iwa/web/routers/olas/funding.py +135 -0
  124. iwa/web/routers/olas/general.py +29 -0
  125. iwa/web/routers/olas/services.py +378 -0
  126. iwa/web/routers/olas/staking.py +341 -0
  127. iwa/web/routers/state.py +65 -0
  128. iwa/web/routers/swap.py +617 -0
  129. iwa/web/routers/transactions.py +153 -0
  130. iwa/web/server.py +155 -0
  131. iwa/web/tests/test_web_endpoints.py +713 -0
  132. iwa/web/tests/test_web_olas.py +430 -0
  133. iwa/web/tests/test_web_swap.py +103 -0
  134. iwa-0.0.1a2.dist-info/METADATA +234 -0
  135. iwa-0.0.1a2.dist-info/RECORD +186 -0
  136. iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
  137. iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
  138. iwa-0.0.1a2.dist-info/top_level.txt +4 -0
  139. tests/legacy_cow.py +248 -0
  140. tests/legacy_safe.py +93 -0
  141. tests/legacy_transaction_retry_logic.py +51 -0
  142. tests/legacy_tui.py +440 -0
  143. tests/legacy_wallets_screen.py +554 -0
  144. tests/legacy_web.py +243 -0
  145. tests/test_account_service.py +120 -0
  146. tests/test_balance_service.py +186 -0
  147. tests/test_chain.py +490 -0
  148. tests/test_chain_interface.py +210 -0
  149. tests/test_cli.py +139 -0
  150. tests/test_contract.py +195 -0
  151. tests/test_db.py +180 -0
  152. tests/test_drain_coverage.py +174 -0
  153. tests/test_erc20.py +95 -0
  154. tests/test_gnosis_plugin.py +111 -0
  155. tests/test_keys.py +449 -0
  156. tests/test_legacy_wallet.py +1285 -0
  157. tests/test_main.py +13 -0
  158. tests/test_mnemonic.py +217 -0
  159. tests/test_modals.py +109 -0
  160. tests/test_models.py +213 -0
  161. tests/test_monitor.py +202 -0
  162. tests/test_multisend.py +84 -0
  163. tests/test_plugin_service.py +119 -0
  164. tests/test_pricing.py +143 -0
  165. tests/test_rate_limiter.py +199 -0
  166. tests/test_reset_tenderly.py +202 -0
  167. tests/test_rpc_view.py +73 -0
  168. tests/test_safe_coverage.py +139 -0
  169. tests/test_safe_service.py +168 -0
  170. tests/test_service_manager_integration.py +61 -0
  171. tests/test_service_manager_structure.py +31 -0
  172. tests/test_service_transaction.py +176 -0
  173. tests/test_staking_router.py +71 -0
  174. tests/test_staking_simple.py +31 -0
  175. tests/test_tables.py +76 -0
  176. tests/test_transaction_service.py +161 -0
  177. tests/test_transfer_multisend.py +179 -0
  178. tests/test_transfer_native.py +220 -0
  179. tests/test_transfer_security.py +93 -0
  180. tests/test_transfer_structure.py +37 -0
  181. tests/test_transfer_swap_unit.py +155 -0
  182. tests/test_ui_coverage.py +66 -0
  183. tests/test_utils.py +53 -0
  184. tests/test_workers.py +91 -0
  185. tools/verify_drain.py +183 -0
  186. __init__.py +0 -2
  187. hello.py +0 -6
  188. iwa-0.0.0.dist-info/METADATA +0 -10
  189. iwa-0.0.0.dist-info/RECORD +0 -6
  190. iwa-0.0.0.dist-info/top_level.txt +0 -2
  191. {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