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,161 @@
1
+ """Tests for TransactionService."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+ from web3 import exceptions as web3_exceptions
7
+
8
+ from iwa.core.keys import EncryptedAccount, KeyStorage
9
+ from iwa.core.services.transaction import TransactionService
10
+
11
+
12
+ @pytest.fixture
13
+ def mock_key_storage():
14
+ """Mock key storage."""
15
+ mock = MagicMock(spec=KeyStorage)
16
+
17
+ # Mock sign_transaction
18
+ mock_signed_tx = MagicMock()
19
+ mock_signed_tx.raw_transaction = b"raw_tx_bytes"
20
+ mock.sign_transaction.return_value = mock_signed_tx
21
+
22
+ return mock
23
+
24
+
25
+ @pytest.fixture
26
+ def mock_account_service():
27
+ """Mock account service."""
28
+ mock = MagicMock()
29
+
30
+ mock_account = MagicMock(spec=EncryptedAccount)
31
+ mock_account.address = "0xSigner"
32
+ mock_account.tag = "signer_tag"
33
+
34
+ mock.resolve_account.return_value = mock_account
35
+ return mock
36
+
37
+
38
+ @pytest.fixture
39
+ def mock_chain_interfaces():
40
+ """Mock chain interfaces."""
41
+ with patch("iwa.core.services.transaction.ChainInterfaces") as mock:
42
+ instance = mock.return_value
43
+ gnosis_interface = MagicMock()
44
+ gnosis_interface.chain.chain_id = 100
45
+
46
+ # Web3 mocks
47
+ gnosis_interface.web3.eth.get_transaction_count.return_value = 5
48
+ gnosis_interface.web3.eth.send_raw_transaction.return_value = b"tx_hash_bytes"
49
+
50
+ # Receipt valid
51
+ mock_receipt = MagicMock()
52
+ mock_receipt.status = 1
53
+ mock_receipt.gasUsed = 21000
54
+ mock_receipt.effectiveGasPrice = 10
55
+ gnosis_interface.web3.eth.wait_for_transaction_receipt.return_value = mock_receipt
56
+
57
+ instance.get.return_value = gnosis_interface
58
+ yield instance
59
+
60
+
61
+ @pytest.fixture
62
+ def mock_external_deps():
63
+ """Mock logger, db, pricing."""
64
+ with (
65
+ patch("iwa.core.services.transaction.log_transaction") as mock_log,
66
+ patch("iwa.core.pricing.PriceService") as mock_price,
67
+ patch("iwa.core.services.transaction.time.sleep") as _, # speed up tests
68
+ ):
69
+ mock_price.return_value.get_token_price.return_value = 1.0 # 1 EUR per Token
70
+ yield {
71
+ "log": mock_log,
72
+ "price": mock_price,
73
+ }
74
+
75
+
76
+ def test_sign_and_send_success(
77
+ mock_key_storage, mock_account_service, mock_chain_interfaces, mock_external_deps
78
+ ):
79
+ """Test successful sign and send flow."""
80
+ service = TransactionService(mock_key_storage, mock_account_service)
81
+
82
+ tx = {"to": "0xDest", "value": 100}
83
+
84
+ success, receipt = service.sign_and_send(tx, "signer")
85
+
86
+ assert success is True
87
+ assert receipt.status == 1
88
+
89
+ # Check flow
90
+ mock_account_service.resolve_account.assert_called_with("signer")
91
+ mock_chain_interfaces.get.assert_called_with("gnosis")
92
+
93
+ # Check nonce filling
94
+ mock_chain_interfaces.get.return_value.web3.eth.get_transaction_count.assert_called()
95
+
96
+ # Check signing
97
+ mock_key_storage.sign_transaction.assert_called()
98
+
99
+ # Check sending
100
+ mock_chain_interfaces.get.return_value.web3.eth.send_raw_transaction.assert_called_with(
101
+ b"raw_tx_bytes"
102
+ )
103
+
104
+ # Check logging
105
+ mock_external_deps["log"].assert_called_once()
106
+ call_args = mock_external_deps["log"].call_args[1]
107
+ assert call_args["tx_hash"] == "74785f686173685f6279746573" # hex of b'tx_hash_bytes'
108
+ assert call_args["tags"] is None
109
+
110
+
111
+ def test_sign_and_send_low_gas_retry(
112
+ mock_key_storage, mock_account_service, mock_chain_interfaces, mock_external_deps
113
+ ):
114
+ """Test retry logic on low gas error."""
115
+ service = TransactionService(mock_key_storage, mock_account_service)
116
+
117
+ web3_mock = mock_chain_interfaces.get.return_value.web3.eth
118
+
119
+ # First attempt fails with "intrinsic gas too low", second succeeds
120
+ web3_mock.send_raw_transaction.side_effect = [
121
+ web3_exceptions.Web3RPCError("intrinsic gas too low"),
122
+ b"tx_hash_bytes_success",
123
+ ]
124
+
125
+ tx = {"to": "0xDest", "value": 100, "gas": 20000}
126
+
127
+ success, receipt = service.sign_and_send(tx, "signer")
128
+
129
+ assert success is True
130
+
131
+ # Check retries
132
+ assert web3_mock.send_raw_transaction.call_count == 2
133
+
134
+ # Verify gas increase
135
+ # Since 'tx' is mutated in place, both mock calls point to the same dict object which now has 30000
136
+ # We can verify that sign_transaction was called twice, and the final gas is 30000
137
+ assert mock_key_storage.sign_transaction.call_count == 2
138
+ final_tx_arg = mock_key_storage.sign_transaction.call_args[0][0]
139
+ assert final_tx_arg["gas"] == 30000
140
+
141
+
142
+ def test_sign_and_send_rpc_rotation(
143
+ mock_key_storage, mock_account_service, mock_chain_interfaces, mock_external_deps
144
+ ):
145
+ """Test RPC rotation on generic error."""
146
+ service = TransactionService(mock_key_storage, mock_account_service)
147
+ chain_interface = mock_chain_interfaces.get.return_value
148
+
149
+ # Side effect: 1. Exception, 2. Success
150
+ chain_interface.web3.eth.send_raw_transaction.side_effect = [
151
+ Exception("Connection reset"),
152
+ b"tx_hash_bytes",
153
+ ]
154
+ chain_interface.rotate_rpc.return_value = True
155
+
156
+ tx = {"to": "0xDest", "value": 100}
157
+
158
+ success, receipt = service.sign_and_send(tx, "signer")
159
+
160
+ assert success is True
161
+ chain_interface.rotate_rpc.assert_called_once()
@@ -0,0 +1,179 @@
1
+ """Tests for TransferService.multi_send."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from iwa.core.constants import NATIVE_CURRENCY_ADDRESS
8
+ from iwa.core.models import StoredSafeAccount
9
+ from iwa.core.services.transfer import TransferService
10
+
11
+
12
+ @pytest.fixture
13
+ def mock_deps():
14
+ """Mock dependencies for TransferService."""
15
+ with (
16
+ patch("iwa.core.services.transfer.base.ChainInterfaces") as mock_chain,
17
+ patch("iwa.core.services.transfer.multisend.ChainInterfaces", new=mock_chain),
18
+ patch("iwa.core.services.transfer.swap.ChainInterfaces", new=mock_chain),
19
+ patch("iwa.core.services.transfer.multisend.MultiSendContract") as mock_ms,
20
+ patch("iwa.core.services.transfer.multisend.MultiSendCallOnlyContract") as mock_ms_co,
21
+ patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
22
+ patch("iwa.core.services.transfer.swap.ERC20Contract", new=mock_erc20),
23
+ patch("iwa.core.services.transfer.erc20.ERC20Contract", new=mock_erc20),
24
+ ):
25
+ mock_account_service = MagicMock()
26
+ mock_key_storage = MagicMock()
27
+ mock_balance_service = MagicMock()
28
+ mock_safe_service = MagicMock()
29
+ mock_txn_service = MagicMock()
30
+ # Set default return for sign_and_send
31
+ mock_txn_service.sign_and_send.return_value = (True, {})
32
+
33
+ # Setup Chain Interface
34
+ mock_w3 = MagicMock()
35
+ # Ensure to_checksum_address returns input if string
36
+ mock_w3.to_checksum_address.side_effect = lambda x: x
37
+ mock_w3.to_wei.return_value = 1000 # 1000 wei default
38
+ mock_chain.return_value.get.return_value.web3 = mock_w3
39
+ mock_chain.return_value.get.return_value.chain.name = "gnosis"
40
+ mock_chain.return_value.get.return_value.chain.tokens = {"TEST": "0xToken"}
41
+ mock_erc20.return_value.allowance_wei.return_value = 0
42
+
43
+ deps = {
44
+ "account_service": mock_account_service,
45
+ "key_storage": mock_key_storage,
46
+ "balance_service": mock_balance_service,
47
+ "safe_service": mock_safe_service,
48
+ "transaction_service": mock_txn_service,
49
+ "contracts": {"ms": mock_ms, "ms_co": mock_ms_co, "erc20": mock_erc20},
50
+ }
51
+ yield deps
52
+
53
+
54
+ def test_multi_send_eoa_native(mock_deps):
55
+ """Test multi_send with EOA and native transfers."""
56
+ service = TransferService(
57
+ mock_deps["account_service"],
58
+ mock_deps["key_storage"],
59
+ mock_deps["balance_service"],
60
+ mock_deps["safe_service"],
61
+ mock_deps["transaction_service"],
62
+ )
63
+
64
+ # Mock From Account (EOA)
65
+ mock_from = MagicMock()
66
+ mock_from.address = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" # Valid checksum address
67
+ mock_from.tag = "from_tag"
68
+
69
+ def resolve_side_effect(arg):
70
+ if arg == "from_tag":
71
+ return mock_from
72
+ return None
73
+
74
+ service.account_service.resolve_account.side_effect = resolve_side_effect
75
+
76
+ # Mock dependencies
77
+ mock_ms_co = mock_deps["contracts"]["ms_co"].return_value
78
+ mock_ms_co.prepare_tx.return_value = {"value": 0, "data": b"encoded"}
79
+ mock_ms_co.address = "0xMultiSendCallOnly"
80
+
81
+ transactions = [
82
+ {
83
+ "to": "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
84
+ "amount": 1.0,
85
+ "token": NATIVE_CURRENCY_ADDRESS,
86
+ },
87
+ {"to": "0xTo2", "amount_wei": 500, "token": NATIVE_CURRENCY_ADDRESS},
88
+ ]
89
+
90
+ service.multi_send("from_tag", transactions)
91
+
92
+ # Verify Account Resolution
93
+ # mock_deps["account_service"].resolve_account.assert_called() # Side effect makes called_with tricky
94
+
95
+ # Verify Contract Interaction
96
+ mock_deps["contracts"]["ms_co"].assert_called()
97
+ mock_ms_co.prepare_tx.assert_called()
98
+
99
+ # Verify Transaction Service called
100
+ mock_deps["transaction_service"].sign_and_send.assert_called()
101
+
102
+
103
+ def test_multi_send_safe_erc20(mock_deps):
104
+ """Test multi_send with Safe and ERC20 transfers."""
105
+ service = TransferService(
106
+ mock_deps["account_service"],
107
+ mock_deps["key_storage"],
108
+ mock_deps["balance_service"],
109
+ mock_deps["safe_service"],
110
+ mock_deps["transaction_service"],
111
+ )
112
+
113
+ # Mock From Account (Safe)
114
+ mock_safe_account = MagicMock(spec=StoredSafeAccount)
115
+ mock_safe_account.address = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
116
+
117
+ service.account_service.resolve_account.side_effect = (
118
+ lambda x: mock_safe_account if x == "safe_tag" else None
119
+ )
120
+
121
+ # Mock ERC20
122
+ mock_erc20 = mock_deps["contracts"]["erc20"].return_value
123
+ mock_erc20.decimals = 18
124
+ mock_erc20.prepare_transfer_tx.return_value = {"data": b"transfer_data"}
125
+ mock_erc20.address = "0xToken"
126
+
127
+ mock_deps["account_service"].get_token_address.return_value = "0xToken"
128
+
129
+ # Mock MultiSend Normal (for Safe)
130
+ mock_ms = mock_deps["contracts"]["ms"].return_value
131
+ mock_ms.prepare_tx.return_value = {"value": 0, "data": b"multisend_data"}
132
+ mock_ms.address = "0xMultiSend"
133
+
134
+ transactions = [{"to": "0xRecipient", "amount": 10.0, "token": "TEST"}]
135
+
136
+ service.multi_send("safe_tag", transactions)
137
+
138
+ # Verify ERC20 prep (Safe uses transfer, not transferFrom)
139
+ mock_erc20.prepare_transfer_tx.assert_called()
140
+
141
+ # Verify Safe Service execution
142
+ mock_deps["safe_service"].execute_safe_transaction.assert_called()
143
+
144
+
145
+ def test_multi_send_eoa_erc20_approval(mock_deps):
146
+ """Test multi_send with EOA checks for allowances."""
147
+ service = TransferService(
148
+ mock_deps["account_service"],
149
+ mock_deps["key_storage"],
150
+ mock_deps["balance_service"],
151
+ mock_deps["safe_service"],
152
+ mock_deps["transaction_service"],
153
+ )
154
+
155
+ # Stub approve_erc20 to verify it's called
156
+ service.approve_erc20 = MagicMock()
157
+
158
+ # Mock From Account (EOA)
159
+ mock_from = MagicMock()
160
+ mock_from.address = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D"
161
+ del mock_from.threshold # EOA has no threshold
162
+ service.account_service.resolve_account.side_effect = (
163
+ lambda x: mock_from if x == "from_tag" else None
164
+ )
165
+
166
+ mock_deps["account_service"].get_token_address.return_value = "0xToken"
167
+ mock_erc20 = mock_deps["contracts"]["erc20"].return_value
168
+ mock_erc20.decimals = 18
169
+ mock_erc20.prepare_transfer_from_tx.return_value = {"data": b"transferFrom"}
170
+
171
+ mock_ms_co = mock_deps["contracts"]["ms_co"].return_value
172
+ mock_ms_co.prepare_tx.return_value = {"value": 0, "data": b"encoded"}
173
+
174
+ transactions = [{"to": "0xRecipient", "amount": 10.0, "token": "TEST"}]
175
+
176
+ service.multi_send("from_tag", transactions)
177
+
178
+ # Verify Approval logic was triggered
179
+ service.approve_erc20.assert_called_once()
@@ -0,0 +1,220 @@
1
+ """Tests for NativeTransferMixin (wrap/unwrap)."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from iwa.core.services.transfer import TransferService
8
+
9
+
10
+ @pytest.fixture
11
+ def mock_deps():
12
+ """Mock dependencies for TransferService."""
13
+ with (
14
+ patch("iwa.core.services.transfer.base.ChainInterfaces") as mock_chain,
15
+ patch("iwa.core.services.transfer.native.ChainInterfaces", new=mock_chain),
16
+ ):
17
+ mock_account_service = MagicMock()
18
+ mock_key_storage = MagicMock()
19
+ mock_balance_service = MagicMock()
20
+ mock_safe_service = MagicMock()
21
+ mock_txn_service = MagicMock()
22
+
23
+ # Setup Chain Interface
24
+ mock_w3 = MagicMock()
25
+ # Mock eth.contract
26
+ mock_contract = MagicMock()
27
+ mock_w3._web3.eth.contract.return_value = mock_contract
28
+ mock_w3._web3.eth.gas_price = 1000000000
29
+ mock_w3._web3.eth.get_transaction_count.return_value = 5
30
+
31
+ # Mock chain info
32
+ mock_chain_instance = mock_chain.return_value.get.return_value
33
+ mock_chain_instance.web3 = mock_w3
34
+ mock_chain_instance.chain.tokens = {"WXDAI": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"}
35
+
36
+ deps = {
37
+ "account_service": mock_account_service,
38
+ "key_storage": mock_key_storage,
39
+ "balance_service": mock_balance_service,
40
+ "safe_service": mock_safe_service,
41
+ "transaction_service": mock_txn_service,
42
+ "chain_interface": mock_chain_instance,
43
+ "contract": mock_contract,
44
+ }
45
+ yield deps
46
+
47
+
48
+ def test_wrap_native_success(mock_deps):
49
+ """Test successful wrap of native currency."""
50
+ service = TransferService(
51
+ mock_deps["account_service"],
52
+ mock_deps["key_storage"],
53
+ mock_deps["balance_service"],
54
+ mock_deps["safe_service"],
55
+ mock_deps["transaction_service"],
56
+ )
57
+
58
+ # Mock Account
59
+ mock_account = MagicMock(name="mock_account")
60
+ mock_account.address = "0xUser"
61
+ service.account_service.resolve_account.return_value = mock_account
62
+
63
+ # Mock Contract Function build_transaction
64
+ mock_function = mock_deps["contract"].functions.deposit.return_value
65
+ mock_function.build_transaction.return_value = {
66
+ "to": "0xWXDAI",
67
+ "value": 1000,
68
+ "data": "0x",
69
+ }
70
+
71
+ # Mock Sign and Send
72
+ mock_signed_tx = MagicMock()
73
+ mock_signed_tx.raw_transaction = b"signed_tx"
74
+ service.key_storage.sign_transaction.return_value = mock_signed_tx
75
+ mock_deps["chain_interface"].web3._web3.eth.send_raw_transaction.return_value = b"tx_hash"
76
+
77
+ # Mock Receipt
78
+ mock_receipt = MagicMock()
79
+ mock_receipt.status = 1
80
+ mock_deps[
81
+ "chain_interface"
82
+ ].web3._web3.eth.wait_for_transaction_receipt.return_value = mock_receipt
83
+
84
+ result = service.wrap_native("user", 1000)
85
+
86
+ assert result == "74785f68617368" # "tx_hash".hex()
87
+ mock_deps["contract"].functions.deposit.assert_called()
88
+ service.key_storage.sign_transaction.assert_called()
89
+
90
+
91
+ def test_wrap_native_account_not_found(mock_deps):
92
+ """Test wrap fails when account not found."""
93
+ service = TransferService(
94
+ mock_deps["account_service"],
95
+ mock_deps["key_storage"],
96
+ mock_deps["balance_service"],
97
+ mock_deps["safe_service"],
98
+ mock_deps["transaction_service"],
99
+ )
100
+ service.account_service.resolve_account.return_value = None
101
+
102
+ result = service.wrap_native("invalid", 1000)
103
+ assert result is None
104
+
105
+
106
+ def test_wrap_native_token_not_found(mock_deps):
107
+ """Test wrap fails when WXDAI token not configured."""
108
+ service = TransferService(
109
+ mock_deps["account_service"],
110
+ mock_deps["key_storage"],
111
+ mock_deps["balance_service"],
112
+ mock_deps["safe_service"],
113
+ mock_deps["transaction_service"],
114
+ )
115
+ service.account_service.resolve_account.return_value = MagicMock()
116
+ mock_deps["chain_interface"].chain.tokens = {} # Empty tokens
117
+
118
+ result = service.wrap_native("user", 1000)
119
+ assert result is None
120
+
121
+
122
+ def test_unwrap_native_success(mock_deps):
123
+ """Test successful unwrap of wrapped token."""
124
+ service = TransferService(
125
+ mock_deps["account_service"],
126
+ mock_deps["key_storage"],
127
+ mock_deps["balance_service"],
128
+ mock_deps["safe_service"],
129
+ mock_deps["transaction_service"],
130
+ )
131
+
132
+ # Mock Account
133
+ mock_account = MagicMock(name="mock_account")
134
+ mock_account.address = "0xUser"
135
+ service.account_service.resolve_account.return_value = mock_account
136
+
137
+ # Mock Contract Function build_transaction
138
+ mock_function = mock_deps["contract"].functions.withdraw.return_value
139
+ mock_function.build_transaction.return_value = {
140
+ "to": "0xWXDAI",
141
+ "data": "0x",
142
+ }
143
+
144
+ # Mock Sign and Send
145
+ mock_signed_tx = MagicMock()
146
+ mock_signed_tx.raw_transaction = b"signed_tx"
147
+ service.key_storage.sign_transaction.return_value = mock_signed_tx
148
+ mock_deps["chain_interface"].web3._web3.eth.send_raw_transaction.return_value = b"tx_hash"
149
+
150
+ # Mock Receipt
151
+ mock_receipt = MagicMock()
152
+ mock_receipt.status = 1
153
+ mock_deps[
154
+ "chain_interface"
155
+ ].web3._web3.eth.wait_for_transaction_receipt.return_value = mock_receipt
156
+
157
+ result = service.unwrap_native("user", 1000)
158
+
159
+ assert result == "74785f68617368"
160
+ mock_deps["contract"].functions.withdraw.assert_called_with(1000)
161
+
162
+
163
+ def test_unwrap_native_auto_balance(mock_deps):
164
+ """Test unwrap with auto-balance detection."""
165
+ service = TransferService(
166
+ mock_deps["account_service"],
167
+ mock_deps["key_storage"],
168
+ mock_deps["balance_service"],
169
+ mock_deps["safe_service"],
170
+ mock_deps["transaction_service"],
171
+ )
172
+
173
+ mock_account = MagicMock(name="mock_account")
174
+ mock_account.address = "0xUser"
175
+ service.account_service.resolve_account.return_value = mock_account
176
+
177
+ # Mock Balance
178
+ mock_deps["balance_service"].get_erc20_balance_wei.return_value = 500
179
+
180
+ # Mock Contract Function
181
+ mock_function = mock_deps["contract"].functions.withdraw.return_value
182
+ mock_function.build_transaction.return_value = {}
183
+
184
+ # Mock Sign/Send/Receipt
185
+ mock_signed_tx = MagicMock()
186
+ mock_signed_tx.raw_transaction = b"signed_tx"
187
+ service.key_storage.sign_transaction.return_value = mock_signed_tx
188
+ mock_deps["chain_interface"].web3._web3.eth.send_raw_transaction.return_value = b"tx_hash"
189
+ mock_receipt = MagicMock()
190
+ mock_receipt.status = 1
191
+ mock_deps[
192
+ "chain_interface"
193
+ ].web3._web3.eth.wait_for_transaction_receipt.return_value = mock_receipt
194
+
195
+ # Call without amount
196
+ result = service.unwrap_native("user")
197
+
198
+ assert result == "74785f68617368"
199
+ mock_deps["balance_service"].get_erc20_balance_wei.assert_called_with(
200
+ "0xUser", "WXDAI", "gnosis"
201
+ )
202
+ mock_deps["contract"].functions.withdraw.assert_called_with(500)
203
+
204
+
205
+ def test_unwrap_native_no_balance(mock_deps):
206
+ """Test unwrap fails when no balance available."""
207
+ service = TransferService(
208
+ mock_deps["account_service"],
209
+ mock_deps["key_storage"],
210
+ mock_deps["balance_service"],
211
+ mock_deps["safe_service"],
212
+ mock_deps["transaction_service"],
213
+ )
214
+
215
+ mock_account = MagicMock()
216
+ service.account_service.resolve_account.return_value = mock_account
217
+ mock_deps["balance_service"].get_erc20_balance_wei.return_value = 0
218
+
219
+ result = service.unwrap_native("user")
220
+ assert result is None
@@ -0,0 +1,93 @@
1
+ """Security tests for TransferService."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+ from eth_account import Account
7
+
8
+ from iwa.core.models import EthereumAddress
9
+ from iwa.core.services.transfer import TransferService
10
+
11
+
12
+ @pytest.fixture
13
+ def transfer_service():
14
+ """Create a TransferService instance with mocked dependencies."""
15
+ return TransferService(
16
+ key_storage=MagicMock(),
17
+ account_service=MagicMock(),
18
+ balance_service=MagicMock(),
19
+ safe_service=MagicMock(),
20
+ transaction_service=MagicMock(),
21
+ )
22
+
23
+
24
+ def test_is_whitelisted_destination_fail_closed(transfer_service):
25
+ """Verify that destination whitelist fails closed (returns False) by default.
26
+
27
+ This addresses TRANS-S1 (Whitelist Bypass).
28
+ """
29
+ # 1. Not an internal account
30
+ transfer_service.account_service.resolve_account.return_value = None
31
+
32
+ # 2. No config whitelist (simulating config.core is None or whitelist empty)
33
+ with patch("iwa.core.services.transfer.base.Config") as mock_config_cls:
34
+ mock_config = MagicMock()
35
+ mock_config.core = None # SIMULATE MISSING CONFIG
36
+ mock_config_cls.return_value = mock_config
37
+
38
+ # Should return False (Blocked)
39
+ random_addr = Account.create().address
40
+ assert transfer_service._is_whitelisted_destination(random_addr) is False
41
+
42
+
43
+ def test_is_whitelisted_destination_explicit_allow(transfer_service):
44
+ """Verify that destination whitelist allows explicitly listed addresses."""
45
+ # 1. Not an internal account
46
+ transfer_service.account_service.resolve_account.return_value = None
47
+
48
+ allowed_addr = Account.create().address
49
+
50
+ # 2. In config whitelist
51
+ with patch("iwa.core.services.transfer.base.Config") as mock_config_cls:
52
+ mock_config = MagicMock()
53
+ mock_config.core.whitelist.values.return_value = [EthereumAddress(allowed_addr)]
54
+ mock_config_cls.return_value = mock_config
55
+
56
+ # Should return True (Allowed)
57
+ assert transfer_service._is_whitelisted_destination(allowed_addr) is True
58
+
59
+
60
+ def test_is_supported_token_strict_validation(transfer_service):
61
+ """Verify that token validation is strict and rejects arbitrary addresses.
62
+
63
+ This addresses TRANS-S2 (Explicit Token Whitelist).
64
+ """
65
+ chain_name = "Gnosis"
66
+ valid_token_addr = Account.create().address
67
+ invalid_token_addr = Account.create().address
68
+
69
+ # Mock chain interface
70
+ mock_chain_interface = MagicMock()
71
+ mock_chain_interface.tokens = {"OLAS": EthereumAddress(valid_token_addr)}
72
+
73
+ with patch("iwa.core.services.transfer.base.ChainInterfaces") as mock_ci_cls:
74
+ mock_ci_cls.return_value.get.return_value = mock_chain_interface
75
+
76
+ # 1. Native currency -> Allowed
77
+ from iwa.core.constants import NATIVE_CURRENCY_ADDRESS
78
+
79
+ assert transfer_service._is_supported_token(NATIVE_CURRENCY_ADDRESS, chain_name) is True
80
+ assert transfer_service._is_supported_token("native", chain_name) is True
81
+
82
+ # 2. Explicitly supported token (ByName) -> Allowed
83
+ assert transfer_service._is_supported_token("OLAS", chain_name) is True
84
+
85
+ # 3. Explicitly supported token (ByAddress) -> Allowed
86
+ assert transfer_service._is_supported_token(valid_token_addr, chain_name) is True
87
+
88
+ # 4. Arbitrary address -> BLOCKED
89
+ # Even if it's a valid ETH address, if it's not in the map, it must be False
90
+ assert transfer_service._is_supported_token(invalid_token_addr, chain_name) is False
91
+
92
+ # 5. Garbage input -> BLOCKED
93
+ assert transfer_service._is_supported_token("NOT_A_TOKEN", chain_name) is False
@@ -0,0 +1,37 @@
1
+ """Test TransferService structure after refactoring."""
2
+
3
+ import inspect
4
+
5
+ from iwa.core.services.transfer import TransferService
6
+
7
+
8
+ def test_transfer_service_structure():
9
+ """Verify TransferService has all expected methods from mixins."""
10
+ service_methods = dict(inspect.getmembers(TransferService, predicate=inspect.isfunction))
11
+
12
+ # Check Base methods
13
+ assert "_resolve_destination" in service_methods
14
+ assert "_calculate_gas_info" in service_methods
15
+
16
+ # Check Native methods
17
+ assert "_send_native_via_safe" in service_methods
18
+ assert "wrap_native" in service_methods
19
+ assert "unwrap_native" in service_methods
20
+
21
+ # Check ERC20 methods
22
+ assert "_send_erc20_via_safe" in service_methods
23
+ assert "get_erc20_allowance" in service_methods
24
+ assert "approve_erc20" in service_methods
25
+ assert "transfer_from_erc20" in service_methods
26
+
27
+ # Check MultiSend/Drain methods
28
+ assert "multi_send" in service_methods
29
+ assert "drain" in service_methods
30
+
31
+ # Check Swap methods
32
+ assert "swap" in service_methods
33
+
34
+ # Check Main methods
35
+ assert "send" in service_methods
36
+
37
+ print("TransferService structure verification passed!")