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,227 @@
1
+ """Tests for CowSwap module."""
2
+
3
+ from unittest.mock import AsyncMock, MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from iwa.core.chain import SupportedChain
8
+ from iwa.plugins.gnosis.cow import CowSwap, OrderType
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_chain():
13
+ """Mock supported chain."""
14
+ mock = MagicMock(spec=SupportedChain)
15
+ mock.chain_id = 100
16
+ mock.name = "Gnosis"
17
+ mock.get_token_address.return_value = "0xToken"
18
+ return mock
19
+
20
+
21
+ @pytest.fixture
22
+ def mock_cowpy_modules():
23
+ """Mock cowpy modules."""
24
+ with (
25
+ patch("iwa.plugins.gnosis.cow.swap.get_cowpy_module") as mock_get_swap,
26
+ patch("iwa.plugins.gnosis.cow.quotes.get_cowpy_module") as mock_get_quotes,
27
+ ):
28
+ # Create mocks for all various modules
29
+ mocks = {
30
+ "SupportedChainId": MagicMock(),
31
+ "Chain": MagicMock(),
32
+ "OrderBookApi": MagicMock(),
33
+ "OrderBookAPIConfigFactory": MagicMock(),
34
+ "get_order_quote": AsyncMock(),
35
+ "OrderQuoteRequest": MagicMock(),
36
+ "OrderQuoteSide3": MagicMock(),
37
+ "OrderQuoteSideKindBuy": MagicMock(),
38
+ "TokenAmount": MagicMock(),
39
+ "OrderQuoteSide1": MagicMock(),
40
+ "OrderQuoteSideKindSell": MagicMock(),
41
+ "Order": MagicMock(),
42
+ "PreSignSignature": MagicMock(),
43
+ "SigningScheme": MagicMock(),
44
+ "sign_order": MagicMock(),
45
+ "post_order": AsyncMock(),
46
+ "CompletedOrder": MagicMock(),
47
+ "swap_tokens": AsyncMock(),
48
+ }
49
+
50
+ # Setup specific returns
51
+ mocks["get_order_quote"].return_value.quote.sellAmount.root = "100"
52
+ mocks["get_order_quote"].return_value.quote.buyAmount.root = "90"
53
+ mocks["get_order_quote"].return_value.quote.validTo = 1234567890
54
+
55
+ mocks["post_order"].return_value = "0xOrderUID"
56
+
57
+ # Correctly mock Chain iteration for get_chain logic
58
+ # chain.value[0] == supported_chain_id (which is mocked as MagicMock by default,
59
+ # but in init it calls SupportedChainId(chain.chain_id))
60
+
61
+ # Let's make supported_chain_id return a specific value and chain matching it
62
+ mock_supported_id = MagicMock()
63
+ mocks["SupportedChainId"].return_value = mock_supported_id
64
+
65
+ mock_chain_enum_item = MagicMock()
66
+ mock_chain_enum_item.value = [mock_supported_id]
67
+
68
+ # Make Chain iterable
69
+ mocks["Chain"].__iter__.return_value = [mock_chain_enum_item]
70
+
71
+ mock_get_swap.side_effect = lambda name: mocks.get(name, MagicMock())
72
+ mock_get_quotes.side_effect = mock_get_swap.side_effect
73
+ yield mocks
74
+
75
+
76
+ @pytest.fixture
77
+ def cowswap(mock_chain, mock_cowpy_modules):
78
+ """CowSwap instance fixture."""
79
+ return CowSwap("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", mock_chain)
80
+
81
+
82
+ def test_init(cowswap, mock_chain):
83
+ """Test initialization."""
84
+ assert cowswap.chain == mock_chain
85
+ assert cowswap.cow_chain is not None
86
+
87
+
88
+ @pytest.mark.asyncio
89
+ async def test_get_max_sell_amount_wei(cowswap, mock_cowpy_modules):
90
+ """Test get_max_sell_amount_wei."""
91
+ amount = await cowswap.get_max_sell_amount_wei(100, "0xSell", "0xBuy")
92
+ # mocked sellAmount root is "100", slippage is 0.005 default -> 100 * 1.005 = 100
93
+ assert amount == 100
94
+ mock_cowpy_modules["get_order_quote"].assert_called_once()
95
+
96
+
97
+ @pytest.mark.asyncio
98
+ async def test_get_max_buy_amount_wei(cowswap, mock_cowpy_modules):
99
+ """Test get_max_buy_amount_wei."""
100
+ amount = await cowswap.get_max_buy_amount_wei(100, "0xSell", "0xBuy")
101
+ # mocked buyAmount root is "90", slippage 0.005 -> 90 * 0.995 = 89.55 -> int 89
102
+ assert amount == 89
103
+ mock_cowpy_modules["get_order_quote"].assert_called_once()
104
+
105
+
106
+ @pytest.mark.asyncio
107
+ async def test_swap_defaults(cowswap, mock_cowpy_modules):
108
+ """Test swap with default settings."""
109
+ # Test SWAP with default logic (using swap_tokens from module)
110
+ # We need to make sure global swap_tokens is None or handled.
111
+ # In test context, we rely on _get_cowpy_module returning the mock.
112
+
113
+ mock_cowpy_modules["swap_tokens"].return_value = MagicMock(uid=MagicMock(root="0x123"))
114
+
115
+ # Mock verify order to return True immediately to avoid sleep
116
+ with patch.object(CowSwap, "check_cowswap_order", return_value={"status": "fulfilled"}):
117
+ result = await cowswap.swap(100, "OLAS", "WXDAI", order_type=OrderType.SELL)
118
+ assert result is not None
119
+ mock_cowpy_modules["swap_tokens"].assert_called()
120
+
121
+
122
+ @pytest.mark.asyncio
123
+ async def test_swap_buy_order_type(cowswap, mock_cowpy_modules):
124
+ """Test swap with BUY order type."""
125
+ # For BUY order type, it uses self.swap_tokens_to_exact_tokens
126
+ # checking patching of global swap_tokens
127
+
128
+ with patch("iwa.plugins.gnosis.cow.swap.swap_tokens", new=None):
129
+ with patch.object(
130
+ CowSwap, "swap_tokens_to_exact_tokens", new_callable=AsyncMock
131
+ ) as mock_custom_swap:
132
+ mock_custom_swap.return_value = MagicMock(uid=MagicMock(root="0x123"))
133
+ with patch.object(CowSwap, "check_cowswap_order", return_value={"status": "fulfilled"}):
134
+ result = await cowswap.swap(100, "OLAS", "WXDAI", order_type=OrderType.BUY)
135
+ assert result is not None
136
+ mock_custom_swap.assert_called()
137
+
138
+
139
+ @pytest.mark.asyncio
140
+ async def test_swap_tokens_to_exact_tokens(cowswap, mock_cowpy_modules):
141
+ """Test swap_tokens_to_exact_tokens custom logic."""
142
+ # Test the custom implementation
143
+ # It calls get_order_quote, post_order
144
+
145
+ mock_cowpy_modules["post_order"].return_value = "0xOrderUID"
146
+ mock_cowpy_modules["OrderBookApi"].return_value.get_order_link.return_value = "http://link"
147
+
148
+ # Mock CompletedOrder to return an object with attributes set from constructor
149
+ def side_effect(uid, url):
150
+ m = MagicMock()
151
+ m.uid = uid
152
+ m.url = url
153
+ return m
154
+
155
+ mock_cowpy_modules["CompletedOrder"].side_effect = side_effect
156
+
157
+ result = await CowSwap.swap_tokens_to_exact_tokens(
158
+ amount=100,
159
+ account=MagicMock(address="0xUser"),
160
+ chain=MagicMock(value=[100]),
161
+ sell_token="0xSell",
162
+ buy_token="0xBuy",
163
+ env="prod",
164
+ )
165
+
166
+ assert result.uid == "0xOrderUID"
167
+ assert result.url == "http://link"
168
+ mock_cowpy_modules["get_order_quote"].assert_called() # Quote needed for sell amount calc
169
+ mock_cowpy_modules["post_order"].assert_called()
170
+
171
+
172
+ @pytest.mark.asyncio
173
+ async def test_check_cowswap_order_success(cowswap):
174
+ """Test check_cowswap_order success path."""
175
+ mock_order = MagicMock()
176
+ mock_order.url = "http://api/order"
177
+
178
+ with patch("requests.get") as mock_get:
179
+ mock_get.return_value.status_code = 200
180
+ mock_get.return_value.json.return_value = {
181
+ "status": "fulfilled",
182
+ "executedSellAmount": "100",
183
+ "executedBuyAmount": "90",
184
+ }
185
+
186
+ # Need to mock loop.run_in_executor since check_cowswap_order uses it
187
+ # Or just let it run if requests.get is mocked?
188
+ # check_cowswap_order calls loop.run_in_executor(None, lambda: requests.get(...))
189
+ # This will run the lambda in a thread. The mock should work.
190
+
191
+ result = await cowswap.check_cowswap_order(mock_order)
192
+
193
+ assert result == {
194
+ "status": "fulfilled",
195
+ "executedSellAmount": "100",
196
+ "executedBuyAmount": "90",
197
+ }
198
+
199
+
200
+ @pytest.mark.asyncio
201
+ async def test_check_cowswap_order_expired(cowswap):
202
+ """Test check_cowswap_order expiration."""
203
+ mock_order = MagicMock()
204
+ mock_order.url = "http://api/order"
205
+
206
+ with patch("requests.get") as mock_get:
207
+ mock_get.return_value.status_code = 200
208
+ mock_get.return_value.json.return_value = {"status": "expired"}
209
+
210
+ result = await cowswap.check_cowswap_order(mock_order)
211
+ assert result is None
212
+
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_check_cowswap_order_timeout(cowswap):
216
+ """Test check_cowswap_order timeout."""
217
+ mock_order = MagicMock()
218
+ mock_order.url = "http://api/order"
219
+
220
+ with patch("requests.get") as mock_get:
221
+ mock_get.return_value.status_code = 200
222
+ mock_get.return_value.json.return_value = {"status": "open", "executedSellAmount": "0"}
223
+
224
+ # Speed up retry sleep (asyncio.sleep)
225
+ with patch("asyncio.sleep", new_callable=AsyncMock):
226
+ result = await cowswap.check_cowswap_order(mock_order)
227
+ assert result is None
@@ -0,0 +1,100 @@
1
+ """Tests for Safe module."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from iwa.core.models import StoredSafeAccount
8
+ from iwa.plugins.gnosis.safe import SafeMultisig
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_settings():
13
+ """Mock settings."""
14
+ with patch("iwa.plugins.gnosis.safe.settings") as mock:
15
+ mock.gnosis_rpc.get_secret_value.return_value = "http://rpc"
16
+ yield mock
17
+
18
+
19
+ @pytest.fixture
20
+ def mock_safe_eth():
21
+ """Mock safe_eth module."""
22
+ with (
23
+ patch("iwa.plugins.gnosis.safe.EthereumClient") as mock_client,
24
+ patch("iwa.plugins.gnosis.safe.Safe") as mock_safe,
25
+ ):
26
+ yield mock_client, mock_safe
27
+
28
+
29
+ @pytest.fixture
30
+ def safe_account():
31
+ """Mock safe account."""
32
+ return StoredSafeAccount(
33
+ address="0x1234567890123456789012345678901234567890",
34
+ owners=["0x1234567890123456789012345678901234567890"],
35
+ threshold=1,
36
+ chains=["gnosis"],
37
+ tag="mysafe",
38
+ signers=[],
39
+ )
40
+
41
+
42
+ def test_init(safe_account, mock_settings, mock_safe_eth):
43
+ """Test initialization."""
44
+ ms = SafeMultisig(safe_account, "gnosis")
45
+ assert ms.multisig is not None
46
+ mock_safe_eth[0].assert_called_with("http://rpc") # EthereumClient init
47
+ mock_safe_eth[1].assert_called() # Safe init
48
+
49
+
50
+ def test_init_invalid_chain(safe_account, mock_settings, mock_safe_eth):
51
+ """Test initialization with invalid chain."""
52
+ with pytest.raises(ValueError, match="not deployed on chain"):
53
+ SafeMultisig(safe_account, "ethereum")
54
+
55
+
56
+ def test_getters(safe_account, mock_settings, mock_safe_eth):
57
+ """Test safe property getters."""
58
+ ms = SafeMultisig(safe_account, "gnosis")
59
+ mock_safe_instance = mock_safe_eth[1].return_value
60
+
61
+ mock_safe_instance.retrieve_owners.return_value = ["0x1"]
62
+ assert ms.get_owners() == ["0x1"]
63
+
64
+ mock_safe_instance.retrieve_threshold.return_value = 2
65
+ assert ms.get_threshold() == 2
66
+
67
+ mock_safe_instance.retrieve_nonce.return_value = 5
68
+ assert ms.get_nonce() == 5
69
+
70
+ mock_safe_instance.retrieve_all_info.return_value = {"info": "test"}
71
+ assert ms.retrieve_all_info() == {"info": "test"}
72
+
73
+
74
+ def test_build_tx(safe_account, mock_settings, mock_safe_eth):
75
+ """Test build_multisig_tx."""
76
+ ms = SafeMultisig(safe_account, "gnosis")
77
+ mock_safe_instance = mock_safe_eth[1].return_value
78
+ mock_safe_instance.build_multisig_tx.return_value = "0xTx"
79
+
80
+ tx = ms.build_tx("0xTo", 100)
81
+ assert tx == "0xTx"
82
+
83
+ mock_safe_instance.build_multisig_tx.assert_called()
84
+
85
+
86
+ def test_send_tx(safe_account, mock_settings, mock_safe_eth):
87
+ """Test send_multisig_tx."""
88
+ ms = SafeMultisig(safe_account, "gnosis")
89
+
90
+ # Mock build_tx just in case (though it delegates)
91
+ # Actually we can let it delegate to mock_safe_instance which returns "0xSafeTx"
92
+ mock_safe_instance = mock_safe_eth[1].return_value
93
+ mock_safe_instance.build_multisig_tx.return_value = "0xSafeTx"
94
+
95
+ callback = MagicMock(return_value="0xHash")
96
+
97
+ tx_hash = ms.send_tx("0xTo", 100, callback)
98
+ assert tx_hash == "0xHash"
99
+
100
+ callback.assert_called_with("0xSafeTx")
@@ -0,0 +1,5 @@
1
+ """iwa.plugins.olas package."""
2
+
3
+ from iwa.plugins.olas.plugin import OlasPlugin
4
+
5
+ __all__ = ["OlasPlugin"]
@@ -0,0 +1,106 @@
1
+ """OLAS protocol constants."""
2
+
3
+ from enum import IntEnum
4
+ from typing import Dict
5
+
6
+ from iwa.core.models import EthereumAddress
7
+
8
+
9
+ class AgentType(IntEnum):
10
+ """Supported OLAS agent types."""
11
+
12
+ TRADER = 25
13
+
14
+
15
+ # Mech Marketplace Payment Types (bytes32 hex strings, without 0x prefix)
16
+ # From mech-client/marketplace_interact.py
17
+ PAYMENT_TYPE_NATIVE = "ba699a34be8fe0e7725e93dcbce1701b0211a8ca61330aaeb8a05bf2ec7abed1"
18
+ PAYMENT_TYPE_TOKEN = "3679d66ef546e66ce9057c4a052f317b135bc8e8c509638f7966edfd4fcf45e9"
19
+ PAYMENT_TYPE_NATIVE_NVM = "803dd08fe79d91027fc9024e254a0942372b92f3ccabc1bd19f4a5c2b251c316"
20
+ PAYMENT_TYPE_TOKEN_NVM_USDC = "0d6fd99afa9c4c580fab5e341922c2a5c4b61d880da60506193d7bf88944dd14"
21
+
22
+ # Mech Factory to Mech Type mappings by chain
23
+ # From mech-client/mech_marketplace_subgraph.py
24
+ MECH_FACTORY_TO_TYPE: Dict[str, Dict[str, str]] = {
25
+ "gnosis": {
26
+ "0x8b299c20F87e3fcBfF0e1B86dC0acC06AB6993EF": "Fixed Price Native",
27
+ "0x31ffDC795FDF36696B8eDF7583A3D115995a45FA": "Fixed Price Token",
28
+ "0x65fd74C29463afe08c879a3020323DD7DF02DA57": "NvmSubscription Native",
29
+ },
30
+ "base": {
31
+ "0x2E008211f34b25A7d7c102403c6C2C3B665a1abe": "Fixed Price Native",
32
+ "0x97371B1C0cDA1D04dFc43DFb50a04645b7Bc9BEe": "Fixed Price Token",
33
+ "0x847bBE8b474e0820215f818858e23F5f5591855A": "NvmSubscription Native",
34
+ "0x7beD01f8482fF686F025628e7780ca6C1f0559fc": "NvmSubscription Token USDC",
35
+ },
36
+ }
37
+
38
+ TRADER_CONFIG_HASH = "108e90795119d6015274ef03af1a669c6d13ab6acc9e2b2978be01ee9ea2ec93"
39
+ DEFAULT_DEPLOY_PAYLOAD = "0x0000000000000000000000000000000000000000{fallback_handler}000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
40
+
41
+ # OLAS Token address on Gnosis chain
42
+ OLAS_TOKEN_ADDRESS_GNOSIS = EthereumAddress("0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f")
43
+
44
+
45
+ # OLAS Protocol Contracts categorized by chain
46
+ # See mech_reference.py for comprehensive documentation of the mech ecosystem
47
+ OLAS_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
48
+ "gnosis": {
49
+ "OLAS_SERVICE_REGISTRY": EthereumAddress("0x9338b5153AE39BB89f50468E608eD9d764B755fD"),
50
+ "OLAS_SERVICE_REGISTRY_TOKEN_UTILITY": EthereumAddress(
51
+ "0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8"
52
+ ),
53
+ "OLAS_SERVICE_MANAGER": EthereumAddress("0x068a4f0946cF8c7f9C1B58a3b5243Ac8843bf473"),
54
+ # Legacy mech - used by TRADER staking contracts for liveness
55
+ "OLAS_MECH": EthereumAddress("0x77af31De935740567Cf4fF1986D04B2c964A786a"),
56
+ # NEW Marketplace (v2) - no staking support yet
57
+ "OLAS_MECH_MARKETPLACE": EthereumAddress("0x735FAAb1c4Ec41128c367AFb5c3baC73509f70bB"),
58
+ # OLD Marketplace (v1) - used by Pearl Beta Mech Marketplace staking
59
+ "OLAS_MECH_MARKETPLACE_OLD": EthereumAddress("0x4554fE75c1f5576c1d7F765B2A036c199Adae329"),
60
+ },
61
+ "ethereum": {
62
+ "OLAS_SERVICE_REGISTRY": EthereumAddress("0x48b6F34dDAf31f94086BFB45e69e0618DDe3677b"),
63
+ "OLAS_SERVICE_MANAGER": EthereumAddress("0x9C14948a39a9c1A58e3f94639908F0076FA715C6"),
64
+ },
65
+ "base": {
66
+ "OLAS_SERVICE_REGISTRY": EthereumAddress("0x3841C312061daB948332A78F042Ec61Ad09fc3D8"),
67
+ "OLAS_SERVICE_MANAGER": EthereumAddress("0xF36183B106692DeD8b6e3B2B7347C9665f8a09B1"),
68
+ "OLAS_MECH_MARKETPLACE": EthereumAddress("0x4554fE75c1f5576c1d7F765B2A036c199Adae329"),
69
+ },
70
+ }
71
+
72
+ # TRADER-compatible staking contracts categorized by chain
73
+ # See https://govern.olas.network/contracts
74
+ # NOTE: All TRADER staking contracts use the LEGACY MECH for activity tracking.
75
+ # The activity checker calls agentMech.getRequestsCount(multisig), where agentMech
76
+ # is hardcoded to the legacy mech (0x77af31De935740567Cf4fF1986D04B2c964A786a).
77
+ #
78
+ # This means:
79
+ # - Legacy mech requests (use_marketplace=False) -> COUNT for liveness rewards
80
+ # - Marketplace mech requests (use_marketplace=True) -> DO NOT COUNT
81
+ #
82
+ # For staking rewards, services MUST use legacy mech requests.
83
+ OLAS_TRADER_STAKING_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
84
+ "gnosis": {
85
+ "Hobbyist 1 (100 OLAS)": EthereumAddress("0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C"),
86
+ "Hobbyist 2 (500 OLAS)": EthereumAddress("0x238EB6993b90A978ec6AAD7530D6429c949C08DA"),
87
+ "Expert (1k OLAS)": EthereumAddress("0x5344B7DD311e5d3DdDd46A4f71481Bd7b05AAA3e"),
88
+ "Expert 2 (1k OLAS)": EthereumAddress("0xb964e44c126410df341ae04B13aB10A985fE3513"),
89
+ "Expert 3 (2k OLAS)": EthereumAddress("0x80faD33Cadb5F53f9D29F02Db97D682E8B101618"),
90
+ "Expert 4 (10k OLAS)": EthereumAddress("0xaD9d891134443B443D7F30013c7e14Fe27F2E029"),
91
+ "Expert 5 (10k OLAS)": EthereumAddress("0xE56dF1E563De1B10715cB313D514af350D207212"),
92
+ "Expert 6 (1k OLAS)": EthereumAddress("0x2546214aEE7eEa4bEE7689C81231017CA231Dc93"),
93
+ "Expert 7 (10k OLAS)": EthereumAddress("0xD7A3C8b975f71030135f1a66E9e23164d54fF455"),
94
+ "Expert 8 (2k OLAS)": EthereumAddress("0x356C108D49C5eebd21c84c04E9162de41933030c"),
95
+ "Expert 9 (10k OLAS)": EthereumAddress("0x17dBAe44BC5618Cc254055B386A29576b4F87015"),
96
+ "Expert 10 (10k OLAS)": EthereumAddress("0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f"),
97
+ "Expert 11 (10k OLAS)": EthereumAddress("0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7"),
98
+ "Expert 12 (10k OLAS)": EthereumAddress("0xF4a75F476801B3fBB2e7093aCDcc3576593Cc1fc"),
99
+ "Expert 15 (10k OLAS)": EthereumAddress("0x88eB38FF79fBa8C19943C0e5Acfa67D5876AdCC1"),
100
+ "Expert 16 (10k OLAS)": EthereumAddress("0x6c65430515c70a3f5E62107CC301685B7D46f991"),
101
+ "Expert 17 (10k OLAS)": EthereumAddress("0x1430107A785C3A36a0C1FC0ee09B9631e2E72aFf"),
102
+ "Expert 18 (10k OLAS)": EthereumAddress("0x041e679d04Fc0D4f75Eb937Dea729Df09a58e454"),
103
+ },
104
+ "ethereum": {},
105
+ "base": {},
106
+ }
@@ -0,0 +1,93 @@
1
+ """Activity checker contract interaction.
2
+
3
+ The MechActivityChecker contract tracks liveness for staked services by monitoring:
4
+ - Safe multisig transaction nonces
5
+ - Mech request counts
6
+
7
+ The liveness check (isRatioPass) verifies that the service is making enough mech
8
+ requests relative to the time elapsed since the last checkpoint.
9
+ """
10
+
11
+ from typing import Tuple
12
+
13
+ from iwa.core.constants import DEFAULT_MECH_CONTRACT_ADDRESS
14
+ from iwa.core.types import EthereumAddress
15
+ from iwa.plugins.olas.contracts.base import OLAS_ABI_PATH, ContractInstance
16
+
17
+
18
+ class ActivityCheckerContract(ContractInstance):
19
+ """Class to interact with the MechActivityChecker contract.
20
+
21
+ This contract tracks mech request activity for staked services and determines
22
+ if they meet the liveness requirements for staking rewards.
23
+
24
+ The getMultisigNonces() function returns an array with two values:
25
+ - nonces[0]: Safe multisig nonce (total transaction count)
26
+ - nonces[1]: Mech requests count (from AgentMech.getRequestsCount)
27
+
28
+ The isRatioPass() function checks if:
29
+ 1. diffRequestsCounts <= diffNonces (requests can't exceed txs)
30
+ 2. ratio = (diffRequestsCounts * 1e18) / time >= livenessRatio
31
+ """
32
+
33
+ name = "activity_checker"
34
+ abi_path = OLAS_ABI_PATH / "activity_checker.json"
35
+
36
+ def __init__(self, address: EthereumAddress, chain_name: str = "gnosis"):
37
+ """Initialize ActivityCheckerContract.
38
+
39
+ Args:
40
+ address: The activity checker contract address.
41
+ chain_name: The chain name (default: gnosis).
42
+
43
+ """
44
+ super().__init__(address, chain_name=chain_name)
45
+
46
+ # Get the mech address this checker tracks
47
+ agent_mech_function = getattr(self.contract.functions, "agentMech", None)
48
+ self.agent_mech = (
49
+ agent_mech_function().call() if agent_mech_function else DEFAULT_MECH_CONTRACT_ADDRESS
50
+ )
51
+
52
+ # Get liveness ratio (requests per second * 1e18)
53
+ self.liveness_ratio = self.contract.functions.livenessRatio().call()
54
+
55
+ def get_multisig_nonces(self, multisig: EthereumAddress) -> Tuple[int, int]:
56
+ """Get the nonces for a multisig address.
57
+
58
+ Args:
59
+ multisig: The multisig address to check.
60
+
61
+ Returns:
62
+ Tuple of (safe_nonce, mech_requests_count):
63
+ - safe_nonce: Total Safe transaction count
64
+ - mech_requests_count: Total mech requests made
65
+
66
+ """
67
+ nonces = self.contract.functions.getMultisigNonces(multisig).call()
68
+ return (nonces[0], nonces[1])
69
+
70
+ def is_ratio_pass(
71
+ self,
72
+ current_nonces: Tuple[int, int],
73
+ last_nonces: Tuple[int, int],
74
+ ts_diff: int,
75
+ ) -> bool:
76
+ """Check if the liveness ratio requirement is passed.
77
+
78
+ The formula checks:
79
+ 1. diffRequestsCounts <= diffNonces (mech requests can't exceed total txs)
80
+ 2. ratio = (diffRequestsCounts * 1e18) / ts_diff >= livenessRatio
81
+
82
+ Args:
83
+ current_nonces: Current (safe_nonce, mech_requests_count).
84
+ last_nonces: Nonces at last checkpoint (safe_nonce, mech_requests_count).
85
+ ts_diff: Time difference in seconds since last checkpoint.
86
+
87
+ Returns:
88
+ True if liveness requirements are met.
89
+
90
+ """
91
+ return self.contract.functions.isRatioPass(
92
+ list(current_nonces), list(last_nonces), ts_diff
93
+ ).call()
@@ -0,0 +1,10 @@
1
+ """Base contract class."""
2
+
3
+ from pathlib import Path
4
+
5
+ from iwa.core.contracts.contract import ContractInstance
6
+
7
+ # OLAS plugin-specific ABI path
8
+ OLAS_ABI_PATH = Path(__file__).parent / "abis"
9
+
10
+ __all__ = ["ContractInstance", "OLAS_ABI_PATH"]
@@ -0,0 +1,49 @@
1
+ """Mech contract interaction."""
2
+
3
+ from typing import Dict, Optional
4
+
5
+ from iwa.core.contracts.contract import ContractInstance
6
+ from iwa.core.types import EthereumAddress
7
+ from iwa.plugins.olas.contracts.base import OLAS_ABI_PATH
8
+
9
+
10
+ class MechContract(ContractInstance):
11
+ """Class to interact with the Mech contract."""
12
+
13
+ def __init__(
14
+ self,
15
+ address: EthereumAddress,
16
+ chain_name: str,
17
+ use_new_abi: bool = False,
18
+ ):
19
+ """Initialize the contract."""
20
+ self.use_new_abi = use_new_abi
21
+ if use_new_abi:
22
+ self.abi_path = OLAS_ABI_PATH / "mech_new.json"
23
+ else:
24
+ self.abi_path = OLAS_ABI_PATH / "mech.json"
25
+ super().__init__(address, chain_name)
26
+
27
+ def get_price(self) -> int:
28
+ """Get the current price for a request."""
29
+ try:
30
+ return self.call("price")
31
+ except Exception:
32
+ # Fallback for new ABIs if price() is not there
33
+ return 10**16 # 0.01 xDAI
34
+
35
+ def prepare_request_tx(
36
+ self,
37
+ from_address: EthereumAddress,
38
+ data: bytes,
39
+ value: Optional[int] = None,
40
+ ) -> Optional[Dict]:
41
+ """Prepare a request transaction."""
42
+ if value is None:
43
+ value = self.get_price()
44
+
45
+ return self.prepare_transaction(
46
+ method_name="request",
47
+ method_kwargs={"data": data},
48
+ tx_params={"from": from_address, "value": value},
49
+ )
@@ -0,0 +1,43 @@
1
+ """Mech Marketplace contract interaction."""
2
+
3
+ from typing import Dict, Optional
4
+
5
+ from iwa.core.contracts.contract import ContractInstance
6
+ from iwa.core.types import EthereumAddress
7
+ from iwa.plugins.olas.contracts.base import OLAS_ABI_PATH
8
+
9
+
10
+ class MechMarketplaceContract(ContractInstance):
11
+ """Class to interact with the Mech Marketplace contract."""
12
+
13
+ name = "mech_marketplace"
14
+ abi_path = OLAS_ABI_PATH / "mech_marketplace.json"
15
+
16
+ def prepare_request_tx(
17
+ self,
18
+ from_address: EthereumAddress,
19
+ request_data: bytes,
20
+ priority_mech: EthereumAddress,
21
+ response_timeout: int = 300,
22
+ max_delivery_rate: int = 10_000,
23
+ payment_type: bytes = b"\x00" * 32,
24
+ payment_data: bytes = b"",
25
+ value: int = 10_000_000_000_000_000, # Default 0.01 xDAI
26
+ ) -> Optional[Dict]:
27
+ """Prepare a marketplace request transaction.
28
+
29
+ Matches ABI:
30
+ request(bytes requestData, uint256 maxDeliveryRate, bytes32 paymentType, address priorityMech, uint256 responseTimeout, bytes paymentData)
31
+ """
32
+ return self.prepare_transaction(
33
+ method_name="request",
34
+ method_kwargs={
35
+ "requestData": request_data,
36
+ "maxDeliveryRate": max_delivery_rate,
37
+ "paymentType": payment_type,
38
+ "priorityMech": priority_mech,
39
+ "responseTimeout": response_timeout,
40
+ "paymentData": payment_data,
41
+ },
42
+ tx_params={"from": from_address, "value": value},
43
+ )