iwa 0.0.0__py3-none-any.whl → 0.0.1a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
tests/test_monitor.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, PropertyMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from web3 import Web3
|
|
5
|
+
|
|
6
|
+
from iwa.core.monitor import EventMonitor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_chain_interfaces():
|
|
11
|
+
with patch("iwa.core.monitor.ChainInterfaces") as mock:
|
|
12
|
+
instance = mock.return_value
|
|
13
|
+
gnosis_interface = MagicMock()
|
|
14
|
+
gnosis_interface.chain.name = "Gnosis"
|
|
15
|
+
gnosis_interface.chain.rpc = "https://rpc"
|
|
16
|
+
gnosis_interface.web3 = MagicMock()
|
|
17
|
+
instance.get.return_value = gnosis_interface
|
|
18
|
+
yield instance
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mock_callback():
|
|
23
|
+
return MagicMock()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_monitor_init_success(mock_chain_interfaces, mock_callback):
|
|
27
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
28
|
+
chain_interface.web3.eth.block_number = 100
|
|
29
|
+
|
|
30
|
+
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
31
|
+
|
|
32
|
+
assert monitor.last_checked_block == 100
|
|
33
|
+
assert monitor.callback == mock_callback
|
|
34
|
+
assert len(monitor.addresses) == 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_monitor_init_rpc_fail(mock_chain_interfaces, mock_callback):
|
|
38
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
39
|
+
# Use PropertyMock to raise exception on attribute access
|
|
40
|
+
type(chain_interface.web3.eth).block_number = PropertyMock(side_effect=Exception("RPC Error"))
|
|
41
|
+
|
|
42
|
+
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
43
|
+
# Should catch exception and set to 0
|
|
44
|
+
assert monitor.last_checked_block == 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_monitor_init_no_rpc(mock_chain_interfaces, mock_callback):
|
|
48
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
49
|
+
chain_interface.chain.rpc = ""
|
|
50
|
+
|
|
51
|
+
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
52
|
+
assert monitor.last_checked_block == 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_start_no_rpc(mock_chain_interfaces, mock_callback):
|
|
56
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
57
|
+
chain_interface.chain.rpc = ""
|
|
58
|
+
|
|
59
|
+
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
60
|
+
|
|
61
|
+
# Patch time.sleep to raise SystemExit if called (invoking cleanup/failure), preventing infinite loop
|
|
62
|
+
# SystemExit is not caught by 'except Exception'
|
|
63
|
+
with patch("time.sleep", side_effect=SystemExit):
|
|
64
|
+
monitor.start()
|
|
65
|
+
|
|
66
|
+
assert monitor.running is False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_check_activity_no_new_block(mock_chain_interfaces, mock_callback):
|
|
70
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
71
|
+
chain_interface.web3.eth.block_number = 100
|
|
72
|
+
|
|
73
|
+
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
74
|
+
monitor.last_checked_block = 100
|
|
75
|
+
|
|
76
|
+
monitor.check_activity()
|
|
77
|
+
|
|
78
|
+
mock_callback.assert_not_called()
|
|
79
|
+
assert monitor.last_checked_block == 100
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_check_activity_block_fetch_failed(mock_chain_interfaces, mock_callback, caplog):
|
|
83
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
84
|
+
# Reset property mock if needed or just use consistent mock
|
|
85
|
+
|
|
86
|
+
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
87
|
+
|
|
88
|
+
# We need to mock block_number raising specifically during check_activity
|
|
89
|
+
# Since we can't easily switch PropertyMock on an instance dynamically without patching the class or instance dict
|
|
90
|
+
# Let's just create a monitor where web3.eth is a specific mock
|
|
91
|
+
|
|
92
|
+
# Actually, patch.object on the INSTANCE attribute works for properties if they are data descriptors or if we patch the class.
|
|
93
|
+
# Easier: Just set the side_effect on the PropertyMock if we used one, or re-assign.
|
|
94
|
+
type(chain_interface.web3.eth).block_number = PropertyMock(side_effect=Exception("RPC Fail"))
|
|
95
|
+
|
|
96
|
+
monitor.check_activity()
|
|
97
|
+
|
|
98
|
+
assert "Failed to get block number" in caplog.text
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_check_activity_new_native_tx(mock_chain_interfaces, mock_callback):
|
|
102
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
103
|
+
chain_interface.web3.eth.block_number = 101
|
|
104
|
+
|
|
105
|
+
# Mock Block
|
|
106
|
+
block = MagicMock()
|
|
107
|
+
block.timestamp = 12345
|
|
108
|
+
# Transaction matching address
|
|
109
|
+
tx = {
|
|
110
|
+
"hash": b"hash",
|
|
111
|
+
"from": "0x1234567890123456789012345678901234567890",
|
|
112
|
+
"to": "0x0000000000000000000000000000000000000000",
|
|
113
|
+
"value": 100,
|
|
114
|
+
}
|
|
115
|
+
block.transactions = [tx]
|
|
116
|
+
|
|
117
|
+
chain_interface.web3.eth.get_block.return_value = block
|
|
118
|
+
chain_interface.web3.eth.get_logs.return_value = [] # No logs
|
|
119
|
+
|
|
120
|
+
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
121
|
+
monitor.last_checked_block = 100
|
|
122
|
+
|
|
123
|
+
monitor.check_activity()
|
|
124
|
+
|
|
125
|
+
assert monitor.last_checked_block == 101
|
|
126
|
+
mock_callback.assert_called_once()
|
|
127
|
+
args, _ = mock_callback.call_args
|
|
128
|
+
found_txs = args[0]
|
|
129
|
+
assert len(found_txs) == 1
|
|
130
|
+
assert found_txs[0]["token"] == "NATIVE"
|
|
131
|
+
assert found_txs[0]["from"] == "0x1234567890123456789012345678901234567890" # Checksummed
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_check_activity_hash_in_block(mock_chain_interfaces, mock_callback):
|
|
135
|
+
# Case where get_block returns tx hash strings instead of objects
|
|
136
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
137
|
+
chain_interface.web3.eth.block_number = 101
|
|
138
|
+
|
|
139
|
+
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
140
|
+
monitor.last_checked_block = 100
|
|
141
|
+
|
|
142
|
+
block = MagicMock()
|
|
143
|
+
block.timestamp = 12345
|
|
144
|
+
block.transactions = [b"hash_bytes"]
|
|
145
|
+
chain_interface.web3.eth.get_block.return_value = block
|
|
146
|
+
|
|
147
|
+
tx_obj = {
|
|
148
|
+
"hash": b"hash_bytes",
|
|
149
|
+
"from": "0x1234567890123456789012345678901234567890",
|
|
150
|
+
"to": "0x0000000000000000000000000000000000000000",
|
|
151
|
+
"value": 100,
|
|
152
|
+
}
|
|
153
|
+
chain_interface.web3.eth.get_transaction.return_value = tx_obj
|
|
154
|
+
chain_interface.web3.eth.get_logs.return_value = []
|
|
155
|
+
|
|
156
|
+
monitor.check_activity()
|
|
157
|
+
|
|
158
|
+
chain_interface.web3.eth.get_transaction.assert_called_with(b"hash_bytes")
|
|
159
|
+
mock_callback.assert_called()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_check_activity_logs(mock_chain_interfaces, mock_callback):
|
|
163
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
164
|
+
chain_interface.web3.eth.block_number = 101
|
|
165
|
+
chain_interface.web3.eth.get_block.return_value = MagicMock(transactions=[])
|
|
166
|
+
|
|
167
|
+
# Mock Log matching address
|
|
168
|
+
|
|
169
|
+
my_addr = "0x1234567890123456789012345678901234567890".lower()
|
|
170
|
+
monitor = EventMonitor([my_addr], mock_callback)
|
|
171
|
+
monitor.last_checked_block = 100
|
|
172
|
+
|
|
173
|
+
# 20 bytes address
|
|
174
|
+
addr_bytes = Web3.to_bytes(hexstr=my_addr) # 20 bytes
|
|
175
|
+
padded_addr_bytes = b"\x00" * 12 + addr_bytes # 32 bytes
|
|
176
|
+
|
|
177
|
+
log = {
|
|
178
|
+
"topics": [
|
|
179
|
+
b"sig",
|
|
180
|
+
b"\x00" * 32, # from (don't care)
|
|
181
|
+
padded_addr_bytes, # to (me) -- This MUST match the padded address logic in monitor.py
|
|
182
|
+
],
|
|
183
|
+
"transactionHash": MagicMock(hex=lambda: "0xloghash"),
|
|
184
|
+
"address": "0xContractAddr",
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
chain_interface.web3.eth.get_logs.side_effect = [[], [log]] # sent, received
|
|
188
|
+
|
|
189
|
+
monitor.check_activity()
|
|
190
|
+
|
|
191
|
+
mock_callback.assert_called()
|
|
192
|
+
found = mock_callback.call_args[0][0]
|
|
193
|
+
assert len(found) == 1
|
|
194
|
+
assert found[0]["token"] == "TOKEN"
|
|
195
|
+
assert found[0]["to"] == my_addr
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_stop(mock_chain_interfaces, mock_callback):
|
|
199
|
+
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
200
|
+
monitor.running = True
|
|
201
|
+
monitor.stop()
|
|
202
|
+
assert monitor.running is False
|
tests/test_multisend.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from safe_eth.safe import SafeOperationEnum
|
|
5
|
+
|
|
6
|
+
from iwa.core.contracts.multisend import MultiSendCallOnlyContract, MultiSendContract
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_contract_instance():
|
|
11
|
+
with (
|
|
12
|
+
patch("iwa.core.contracts.contract.ContractInstance.__init__", return_value=None),
|
|
13
|
+
patch("iwa.core.contracts.contract.ContractInstance.call") as mock_call,
|
|
14
|
+
patch("iwa.core.contracts.contract.ContractInstance.prepare_transaction") as mock_prep,
|
|
15
|
+
):
|
|
16
|
+
yield mock_call, mock_prep
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_encode_data():
|
|
20
|
+
tx = {
|
|
21
|
+
"operation": SafeOperationEnum.CALL,
|
|
22
|
+
"to": "0x1111111111111111111111111111111111111111",
|
|
23
|
+
"value": 100,
|
|
24
|
+
"data": b"\x01\x02",
|
|
25
|
+
}
|
|
26
|
+
encoded = MultiSendCallOnlyContract.encode_data(tx)
|
|
27
|
+
# Operation (1 byte) + To (20 bytes) + Value (32 bytes) + Data Length (32 bytes) + Data (2 bytes)
|
|
28
|
+
# But wait, implementation uses HexBytes formatting which might produce different output if not careful.
|
|
29
|
+
# Let's check length.
|
|
30
|
+
# 1 + 20 + 32 + 32 + 2 = 87 bytes.
|
|
31
|
+
assert len(encoded) == 87
|
|
32
|
+
assert encoded[0] == 0 # CALL is 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_to_bytes():
|
|
36
|
+
tx1 = {
|
|
37
|
+
"operation": SafeOperationEnum.CALL,
|
|
38
|
+
"to": "0x1111111111111111111111111111111111111111",
|
|
39
|
+
"value": 100,
|
|
40
|
+
"data": b"\x01",
|
|
41
|
+
}
|
|
42
|
+
tx2 = {
|
|
43
|
+
"operation": SafeOperationEnum.DELEGATE_CALL,
|
|
44
|
+
"to": "0x2222222222222222222222222222222222222222",
|
|
45
|
+
"value": 0,
|
|
46
|
+
"data": b"",
|
|
47
|
+
}
|
|
48
|
+
encoded = MultiSendCallOnlyContract.to_bytes([tx1, tx2])
|
|
49
|
+
# tx1: 1+20+32+32+1 = 86
|
|
50
|
+
# tx2: 1+20+32+32+0 = 85
|
|
51
|
+
# Total: 171
|
|
52
|
+
assert len(encoded) == 171
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_prepare_tx(mock_contract_instance):
|
|
56
|
+
mock_call, mock_prep = mock_contract_instance
|
|
57
|
+
mock_prep.return_value = {"data": "0x"}
|
|
58
|
+
|
|
59
|
+
multisend = MultiSendCallOnlyContract("0xMulti", "gnosis")
|
|
60
|
+
|
|
61
|
+
transactions = [
|
|
62
|
+
{
|
|
63
|
+
"operation": SafeOperationEnum.CALL,
|
|
64
|
+
"to": "0x1111111111111111111111111111111111111111",
|
|
65
|
+
"value": 100,
|
|
66
|
+
"data": b"",
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
tx = multisend.prepare_tx("0xFrom", transactions)
|
|
71
|
+
assert tx == {"data": "0x"}
|
|
72
|
+
|
|
73
|
+
mock_prep.assert_called_once()
|
|
74
|
+
call_args = mock_prep.call_args[1]
|
|
75
|
+
assert call_args["method_name"] == "multiSend"
|
|
76
|
+
assert "encoded_multisend_data" in call_args["method_kwargs"]
|
|
77
|
+
assert call_args["tx_params"]["from"] == "0xFrom"
|
|
78
|
+
assert call_args["tx_params"]["value"] == 100
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_multisend_contract_init(mock_contract_instance):
|
|
82
|
+
# Just verify it can be instantiated and has correct name
|
|
83
|
+
ms = MultiSendContract("0xMulti", "gnosis")
|
|
84
|
+
assert ms.name == "multisend"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Tests for PluginService."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from iwa.core.plugins import Plugin
|
|
6
|
+
from iwa.core.services.plugin import PluginService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MockPlugin(Plugin):
|
|
10
|
+
"""Mock plugin for testing."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def name(self):
|
|
14
|
+
return "mock_plugin"
|
|
15
|
+
|
|
16
|
+
def get_cli_commands(self):
|
|
17
|
+
return {"mock": lambda: None}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_plugin_service_init():
|
|
21
|
+
"""Test PluginService initialization loads plugins."""
|
|
22
|
+
with patch.object(PluginService, "_load_plugins"):
|
|
23
|
+
service = PluginService()
|
|
24
|
+
assert service.plugins_package == "iwa.plugins"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_discover_plugins_import_error():
|
|
28
|
+
"""Test _discover_plugins handles ImportError."""
|
|
29
|
+
with patch.object(PluginService, "_load_plugins"):
|
|
30
|
+
service = PluginService()
|
|
31
|
+
|
|
32
|
+
with patch(
|
|
33
|
+
"iwa.core.services.plugin.importlib.import_module",
|
|
34
|
+
side_effect=ImportError("Module not found"),
|
|
35
|
+
):
|
|
36
|
+
plugins = service._discover_plugins()
|
|
37
|
+
assert plugins == []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_discover_plugins_no_path():
|
|
41
|
+
"""Test _discover_plugins handles package without __path__."""
|
|
42
|
+
with patch.object(PluginService, "_load_plugins"):
|
|
43
|
+
service = PluginService()
|
|
44
|
+
|
|
45
|
+
with patch("iwa.core.services.plugin.importlib.import_module") as mock_import:
|
|
46
|
+
mock_package = MagicMock(spec=[]) # No __path__
|
|
47
|
+
del mock_package.__path__ # Ensure hasattr returns False
|
|
48
|
+
mock_import.return_value = mock_package
|
|
49
|
+
|
|
50
|
+
plugins = service._discover_plugins()
|
|
51
|
+
assert plugins == []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_load_plugins_module_error():
|
|
55
|
+
"""Test _load_plugins handles module import errors."""
|
|
56
|
+
with (
|
|
57
|
+
patch.object(PluginService, "_discover_plugins", return_value=["bad_module"]),
|
|
58
|
+
patch(
|
|
59
|
+
"iwa.core.services.plugin.importlib.import_module",
|
|
60
|
+
side_effect=ImportError("Bad module"),
|
|
61
|
+
),
|
|
62
|
+
):
|
|
63
|
+
# Should not raise, just log error
|
|
64
|
+
service = PluginService()
|
|
65
|
+
|
|
66
|
+
assert "bad_module" not in service.loaded_plugins
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_get_plugin():
|
|
70
|
+
"""Test get_plugin returns correct plugin."""
|
|
71
|
+
with patch.object(PluginService, "_load_plugins"):
|
|
72
|
+
service = PluginService()
|
|
73
|
+
mock_plugin = MockPlugin()
|
|
74
|
+
service.loaded_plugins["mock_plugin"] = mock_plugin
|
|
75
|
+
|
|
76
|
+
result = service.get_plugin("mock_plugin")
|
|
77
|
+
|
|
78
|
+
assert result == mock_plugin
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_get_plugin_not_found():
|
|
82
|
+
"""Test get_plugin returns None for unknown plugin."""
|
|
83
|
+
with patch.object(PluginService, "_load_plugins"):
|
|
84
|
+
service = PluginService()
|
|
85
|
+
|
|
86
|
+
result = service.get_plugin("nonexistent")
|
|
87
|
+
|
|
88
|
+
assert result is None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_get_all_plugins():
|
|
92
|
+
"""Test get_all_plugins returns copy of plugins."""
|
|
93
|
+
with patch.object(PluginService, "_load_plugins"):
|
|
94
|
+
service = PluginService()
|
|
95
|
+
mock_plugin = MockPlugin()
|
|
96
|
+
service.loaded_plugins["mock_plugin"] = mock_plugin
|
|
97
|
+
|
|
98
|
+
result = service.get_all_plugins()
|
|
99
|
+
|
|
100
|
+
assert "mock_plugin" in result
|
|
101
|
+
# Verify it's a copy
|
|
102
|
+
result["new_plugin"] = None
|
|
103
|
+
assert "new_plugin" not in service.loaded_plugins
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_skip_already_loaded():
|
|
107
|
+
"""Test _load_plugins skips already loaded plugins."""
|
|
108
|
+
with patch.object(PluginService, "_discover_plugins", return_value=["mock"]):
|
|
109
|
+
with patch.object(PluginService, "_load_plugins"):
|
|
110
|
+
service = PluginService()
|
|
111
|
+
|
|
112
|
+
# Pre-populate loaded plugins with key matching discovered name
|
|
113
|
+
mock_plugin = MockPlugin()
|
|
114
|
+
service.loaded_plugins["mock"] = mock_plugin
|
|
115
|
+
|
|
116
|
+
# Now load - should skip "mock" since it's in loaded_plugins
|
|
117
|
+
with patch("iwa.core.services.plugin.importlib.import_module") as mock_import:
|
|
118
|
+
service._load_plugins()
|
|
119
|
+
mock_import.assert_not_called()
|
tests/test_pricing.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from iwa.core.pricing import PriceService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_secrets():
|
|
11
|
+
with patch("iwa.core.pricing.settings") as mock:
|
|
12
|
+
mock.coingecko_api_key.get_secret_value.return_value = "test_api_key"
|
|
13
|
+
yield mock
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def price_service(mock_secrets):
|
|
18
|
+
return PriceService()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_get_token_price_success(price_service):
|
|
22
|
+
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
23
|
+
mock_get.return_value.status_code = 200
|
|
24
|
+
mock_get.return_value.json.return_value = {"ethereum": {"eur": 2000.50}}
|
|
25
|
+
|
|
26
|
+
price = price_service.get_token_price("ethereum", "eur")
|
|
27
|
+
|
|
28
|
+
assert price == 2000.50
|
|
29
|
+
mock_get.assert_called_once()
|
|
30
|
+
# Verify API key in headers
|
|
31
|
+
args, kwargs = mock_get.call_args
|
|
32
|
+
assert kwargs["headers"]["x-cg-demo-api-key"] == "test_api_key"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_get_token_price_cached(price_service):
|
|
36
|
+
# Pre-populate cache
|
|
37
|
+
from datetime import datetime
|
|
38
|
+
|
|
39
|
+
price_service.cache["ethereum_eur"] = {"price": 100.0, "timestamp": datetime.now()}
|
|
40
|
+
|
|
41
|
+
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
42
|
+
price = price_service.get_token_price("ethereum", "eur")
|
|
43
|
+
assert price == 100.0
|
|
44
|
+
mock_get.assert_not_called()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_get_token_price_cache_expired(price_service):
|
|
48
|
+
# Pre-populate expired cache
|
|
49
|
+
from datetime import datetime
|
|
50
|
+
|
|
51
|
+
price_service.cache["ethereum_eur"] = {
|
|
52
|
+
"price": 100.0,
|
|
53
|
+
"timestamp": datetime.now() - timedelta(minutes=10),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
57
|
+
mock_get.return_value.status_code = 200
|
|
58
|
+
mock_get.return_value.json.return_value = {"ethereum": {"eur": 200.0}}
|
|
59
|
+
|
|
60
|
+
price = price_service.get_token_price("ethereum", "eur")
|
|
61
|
+
assert price == 200.0
|
|
62
|
+
mock_get.assert_called_once()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_get_token_price_api_error(price_service):
|
|
66
|
+
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
67
|
+
mock_get.side_effect = Exception("API Error")
|
|
68
|
+
|
|
69
|
+
price = price_service.get_token_price("ethereum", "eur")
|
|
70
|
+
assert price is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_get_token_price_key_not_found(price_service):
|
|
74
|
+
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
75
|
+
mock_get.return_value.status_code = 200
|
|
76
|
+
mock_get.return_value.json.return_value = {} # Empty response
|
|
77
|
+
|
|
78
|
+
price = price_service.get_token_price("ethereum", "eur")
|
|
79
|
+
assert price is None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_get_token_price_rate_limit():
|
|
83
|
+
"""Test rate limit (429) handling with retries."""
|
|
84
|
+
with patch("iwa.core.pricing.settings") as mock_settings:
|
|
85
|
+
mock_settings.coingecko_api_key = None
|
|
86
|
+
|
|
87
|
+
service = PriceService()
|
|
88
|
+
|
|
89
|
+
with patch("iwa.core.pricing.requests.get") as mock_get, patch("time.sleep"):
|
|
90
|
+
# Return 429 for all attempts
|
|
91
|
+
mock_response = type("Response", (), {"status_code": 429})()
|
|
92
|
+
mock_get.return_value = mock_response
|
|
93
|
+
|
|
94
|
+
price = service.get_token_price("ethereum", "eur")
|
|
95
|
+
|
|
96
|
+
assert price is None
|
|
97
|
+
# Should have tried max_retries + 1 times
|
|
98
|
+
assert mock_get.call_count == 3
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_get_token_price_rate_limit_then_success():
|
|
102
|
+
"""Test rate limit recovery on retry."""
|
|
103
|
+
from unittest.mock import MagicMock
|
|
104
|
+
|
|
105
|
+
with patch("iwa.core.pricing.settings") as mock_settings:
|
|
106
|
+
mock_settings.coingecko_api_key = None
|
|
107
|
+
|
|
108
|
+
service = PriceService()
|
|
109
|
+
|
|
110
|
+
with patch("iwa.core.pricing.requests.get") as mock_get, patch("time.sleep"):
|
|
111
|
+
# First call returns 429, second succeeds
|
|
112
|
+
mock_429 = MagicMock()
|
|
113
|
+
mock_429.status_code = 429
|
|
114
|
+
|
|
115
|
+
mock_ok = MagicMock()
|
|
116
|
+
mock_ok.status_code = 200
|
|
117
|
+
mock_ok.json.return_value = {"ethereum": {"eur": 1500.0}}
|
|
118
|
+
|
|
119
|
+
mock_get.side_effect = [mock_429, mock_ok]
|
|
120
|
+
|
|
121
|
+
price = service.get_token_price("ethereum", "eur")
|
|
122
|
+
|
|
123
|
+
assert price == 1500.0
|
|
124
|
+
assert mock_get.call_count == 2
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_get_token_price_no_api_key():
|
|
128
|
+
"""Test getting price without API key."""
|
|
129
|
+
with patch("iwa.core.pricing.settings") as mock_settings:
|
|
130
|
+
mock_settings.coingecko_api_key = None
|
|
131
|
+
|
|
132
|
+
service = PriceService()
|
|
133
|
+
|
|
134
|
+
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
135
|
+
mock_get.return_value.status_code = 200
|
|
136
|
+
mock_get.return_value.json.return_value = {"gnosis": {"eur": 100.0}}
|
|
137
|
+
|
|
138
|
+
price = service.get_token_price("gnosis", "eur")
|
|
139
|
+
|
|
140
|
+
assert price == 100.0
|
|
141
|
+
# Verify no API key header
|
|
142
|
+
args, kwargs = mock_get.call_args
|
|
143
|
+
assert "x-cg-demo-api-key" not in kwargs.get("headers", {})
|