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,341 @@
1
+ """Olas Staking Router."""
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, Request
7
+ from slowapi import Limiter
8
+ from slowapi.util import get_remote_address
9
+
10
+ from iwa.core.models import Config
11
+ from iwa.plugins.olas.models import OlasConfig
12
+ from iwa.web.dependencies import get_config, verify_auth, wallet
13
+
14
+ logger = logging.getLogger(__name__)
15
+ router = APIRouter(tags=["olas"])
16
+ limiter = Limiter(key_func=get_remote_address)
17
+
18
+
19
+ @router.get(
20
+ "/staking-contracts",
21
+ summary="Get Staking Contracts",
22
+ description="Get the list of available OLAS staking contracts for a specific chain.",
23
+ )
24
+ def get_staking_contracts(
25
+ chain: str = "gnosis",
26
+ service_key: Optional[str] = None,
27
+ auth: bool = Depends(verify_auth), # noqa: B008
28
+ config: Config = Depends(get_config), # noqa: B008
29
+ ):
30
+ """Get available staking contracts for a chain, optionally filtered by service bond."""
31
+ if not chain.replace("-", "").isalnum():
32
+ from fastapi import HTTPException
33
+
34
+ raise HTTPException(status_code=400, detail="Invalid chain name")
35
+
36
+ try:
37
+ import json
38
+
39
+ from iwa.core.chain import ChainInterface
40
+ from iwa.plugins.olas.constants import OLAS_TRADER_STAKING_CONTRACTS
41
+ from iwa.plugins.olas.contracts.base import OLAS_ABI_PATH
42
+
43
+ contracts = OLAS_TRADER_STAKING_CONTRACTS.get(chain, {})
44
+
45
+ # Get service bond and token if filtered
46
+ service_bond, service_token = _get_service_filter_info(service_key)
47
+
48
+ # Load ABI once
49
+ with open(OLAS_ABI_PATH / "staking.json", "r") as f:
50
+ abi = json.load(f)
51
+
52
+ # Get correct web3 instance
53
+ w3 = ChainInterface(chain).web3
54
+
55
+ results = _fetch_all_contracts(contracts, w3, abi)
56
+ filtered_results = _filter_contracts(results, service_bond, service_token)
57
+
58
+ # Return with filter metadata so frontend can explain filtering
59
+ return {
60
+ "contracts": filtered_results,
61
+ "filter_info": {
62
+ "service_bond": service_bond,
63
+ "service_bond_olas": service_bond / 10**18 if service_bond else None,
64
+ "total_contracts": len(results),
65
+ "filtered_count": len(filtered_results),
66
+ "is_filtered": service_key is not None,
67
+ },
68
+ }
69
+
70
+ except Exception as e:
71
+ import traceback
72
+
73
+ traceback.print_exc()
74
+ logger.error(f"Error fetching staking contracts: {e}")
75
+ return []
76
+
77
+
78
+ def _get_service_filter_info(service_key: Optional[str]) -> tuple[Optional[int], Optional[str]]:
79
+ """Retrieve service bond and token if service_key is provided."""
80
+ service_bond = None
81
+ service_token = None
82
+
83
+ if service_key:
84
+ try:
85
+ from iwa.plugins.olas.service_manager import ServiceManager
86
+
87
+ # Initialize wallet dependencies for ServiceManager
88
+ manager = ServiceManager(wallet, service_key)
89
+ if manager.service:
90
+ # Get service requirements
91
+ service_token = (manager.service.token_address or "").lower()
92
+ service_id_int = manager.service.service_id
93
+
94
+ # Get security deposit from registry - this is the actual bond value
95
+ try:
96
+ service_info = manager.registry.get_service(service_id_int)
97
+ service_bond = service_info.get("security_deposit", 0)
98
+ logger.info(
99
+ f"Filtering for service {service_key}: security_deposit={service_bond}, token={service_token}"
100
+ )
101
+ except Exception as e:
102
+ logger.warning(f"Failed to get service info for filtering: {e}")
103
+
104
+ except Exception as e:
105
+ logger.warning(f"Could not fetch service details for filtering: {e}")
106
+ # Don't fail the request, just skip filtering
107
+ pass
108
+
109
+ return service_bond, service_token
110
+
111
+
112
+ def _check_availability(name, address, w3, abi):
113
+ """Check availability of a single staking contract."""
114
+ try:
115
+ contract = w3.eth.contract(address=address, abi=abi)
116
+ service_ids = contract.functions.getServiceIds().call()
117
+ max_services = contract.functions.maxNumServices().call()
118
+ min_deposit = contract.functions.minStakingDeposit().call()
119
+ staking_token = contract.functions.stakingToken().call()
120
+ used = len(service_ids)
121
+
122
+ return {
123
+ "name": name,
124
+ "address": address,
125
+ "usage": {
126
+ "used": used,
127
+ "max": max_services,
128
+ "available_slots": max_services - used,
129
+ "available": used < max_services,
130
+ },
131
+ "min_staking_deposit": min_deposit,
132
+ "staking_token": staking_token,
133
+ }
134
+ except Exception as e:
135
+ logger.warning(f"Failed to check availability for {name} ({address}): {e}")
136
+ return {
137
+ "name": name,
138
+ "address": address,
139
+ "usage": None, # Could not verify
140
+ "min_staking_deposit": None,
141
+ }
142
+
143
+
144
+ def _fetch_all_contracts(contracts: dict, w3, abi) -> list:
145
+ """Fetch availability for all contracts using threads."""
146
+ from concurrent.futures import ThreadPoolExecutor
147
+
148
+ results = []
149
+ with ThreadPoolExecutor(max_workers=10) as executor:
150
+ # Pass w3 and abi to the helper
151
+ futures = [
152
+ executor.submit(_check_availability, name, addr, w3, abi)
153
+ for name, addr in contracts.items()
154
+ ]
155
+ for future in futures:
156
+ results.append(future.result())
157
+ return results
158
+
159
+
160
+ def _filter_contracts(
161
+ results: list, service_bond: Optional[int], service_token: Optional[str]
162
+ ) -> list:
163
+ """Filter contracts based on usage and service compatibility."""
164
+ filtered_results = []
165
+ for r in results:
166
+ # 1. Availability check
167
+ if r["usage"] is not None and not r["usage"]["available"]:
168
+ continue
169
+
170
+ # 2. Compatibility check (if service info is known)
171
+ if service_bond is not None and r.get("min_staking_deposit") is not None:
172
+ # Bond Check
173
+ if service_bond < r["min_staking_deposit"]:
174
+ # Incompatible: Service bond is too low for this contract
175
+ continue
176
+
177
+ # Token Check
178
+ contract_token = str(r.get("staking_token", "")).lower()
179
+ if service_token and contract_token and service_token != contract_token:
180
+ # Incompatible: Tokens do not match
181
+ continue
182
+
183
+ filtered_results.append(r)
184
+ return filtered_results
185
+
186
+
187
+ @router.post(
188
+ "/stake/{service_key}",
189
+ summary="Stake Service",
190
+ description="Stake a service into a staking contract.",
191
+ )
192
+ @limiter.limit("5/minute")
193
+ def stake_service(
194
+ request: Request,
195
+ service_key: str,
196
+ staking_contract: str,
197
+ auth: bool = Depends(verify_auth),
198
+ ):
199
+ """Stake a service into a staking contract."""
200
+ try:
201
+ from iwa.plugins.olas.contracts.staking import StakingContract
202
+ from iwa.plugins.olas.service_manager import ServiceManager
203
+
204
+ config = Config()
205
+ olas_config = OlasConfig.model_validate(config.plugins["olas"])
206
+ service = olas_config.services.get(service_key)
207
+
208
+ if not service:
209
+ raise HTTPException(status_code=404, detail="Service not found")
210
+
211
+ manager = ServiceManager(wallet)
212
+ manager.service = service
213
+
214
+ # Ensure staking_contract is a valid address format
215
+ if not staking_contract.startswith("0x"):
216
+ raise HTTPException(
217
+ status_code=400, detail=f"Invalid staking contract address: {staking_contract}"
218
+ )
219
+
220
+ staking = StakingContract(staking_contract, service.chain_name)
221
+ success = manager.stake(staking)
222
+
223
+ if success:
224
+ return {"status": "success"}
225
+ else:
226
+ raise HTTPException(status_code=400, detail="Failed to stake service")
227
+
228
+ except HTTPException:
229
+ raise
230
+ except Exception as e:
231
+ logger.error(f"Error staking service: {e}")
232
+ raise HTTPException(status_code=400, detail=str(e)) from None
233
+
234
+
235
+ @router.post(
236
+ "/claim/{service_key}",
237
+ summary="Claim Rewards",
238
+ description="Claim accrued staking rewards for a specific service.",
239
+ )
240
+ def claim_rewards(service_key: str, auth: bool = Depends(verify_auth)):
241
+ """Claim accrued staking rewards for a service."""
242
+ try:
243
+ from iwa.plugins.olas.service_manager import ServiceManager
244
+
245
+ config = Config()
246
+ olas_config = OlasConfig.model_validate(config.plugins["olas"])
247
+ service = olas_config.services.get(service_key)
248
+
249
+ if not service:
250
+ raise HTTPException(status_code=404, detail="Service not found")
251
+
252
+ manager = ServiceManager(wallet)
253
+ manager.service = service
254
+
255
+ success, amount = manager.claim_rewards()
256
+ if success:
257
+ return {"status": "success", "amount": amount}
258
+ else:
259
+ raise HTTPException(status_code=400, detail="Failed to claim rewards")
260
+
261
+ except HTTPException:
262
+ raise
263
+ except Exception as e:
264
+ logger.error(f"Error claiming rewards: {e}")
265
+ raise HTTPException(status_code=500, detail=str(e)) from None
266
+
267
+
268
+ @router.post(
269
+ "/unstake/{service_key}",
270
+ summary="Unstake Service",
271
+ description="Unstake a service from the registry.",
272
+ )
273
+ def unstake_service(service_key: str, auth: bool = Depends(verify_auth)):
274
+ """Unstake a service."""
275
+ try:
276
+ from iwa.plugins.olas.contracts.staking import StakingContract
277
+ from iwa.plugins.olas.service_manager import ServiceManager
278
+
279
+ config = Config()
280
+ olas_config = OlasConfig.model_validate(config.plugins["olas"])
281
+ service = olas_config.services.get(service_key)
282
+
283
+ if not service or not service.staking_contract_address:
284
+ raise HTTPException(status_code=404, detail="Service not found or not staked")
285
+
286
+ manager = ServiceManager(wallet)
287
+ manager.service = service
288
+
289
+ # We need the staking contract instance
290
+ staking_contract = StakingContract(service.staking_contract_address, service.chain_name)
291
+
292
+ success = manager.unstake(staking_contract)
293
+ if success:
294
+ return {"status": "success"}
295
+ else:
296
+ raise HTTPException(status_code=400, detail="Failed to unstake")
297
+
298
+ except HTTPException:
299
+ raise
300
+ except Exception as e:
301
+ logger.error(f"Error unstaking: {e}")
302
+ raise HTTPException(status_code=500, detail=str(e)) from None
303
+
304
+
305
+ @router.post(
306
+ "/checkpoint/{service_key}",
307
+ summary="Checkpoint Service",
308
+ description="Trigger a checkpoint for a staked service to update its liveness.",
309
+ )
310
+ def checkpoint_service(service_key: str, auth: bool = Depends(verify_auth)):
311
+ """Checkpoint a service."""
312
+ try:
313
+ from iwa.plugins.olas.contracts.staking import StakingContract
314
+ from iwa.plugins.olas.service_manager import ServiceManager
315
+
316
+ config = Config()
317
+ olas_config = OlasConfig.model_validate(config.plugins["olas"])
318
+ service = olas_config.services.get(service_key)
319
+
320
+ if not service or not service.staking_contract_address:
321
+ raise HTTPException(status_code=404, detail="Service not found or not staked")
322
+
323
+ manager = ServiceManager(wallet)
324
+ manager.service = service
325
+
326
+ staking_contract = StakingContract(service.staking_contract_address, service.chain_name)
327
+
328
+ success = manager.call_checkpoint(staking_contract)
329
+ if success:
330
+ return {"status": "success"}
331
+ else:
332
+ # Check if it was just not needed
333
+ if not staking_contract.is_checkpoint_needed():
334
+ return {"status": "skipped", "message": "Checkpoint not needed yet"}
335
+ raise HTTPException(status_code=400, detail="Failed to checkpoint")
336
+
337
+ except HTTPException:
338
+ raise
339
+ except Exception as e:
340
+ logger.error(f"Error checkpointing: {e}")
341
+ raise HTTPException(status_code=500, detail=str(e)) from None
@@ -0,0 +1,65 @@
1
+ """State Router for Web API."""
2
+
3
+ from fastapi import APIRouter, Depends
4
+
5
+ from iwa.core.chain import ChainInterfaces
6
+ from iwa.web.dependencies import verify_auth
7
+
8
+ router = APIRouter(prefix="/api", tags=["state"])
9
+
10
+
11
+ @router.get(
12
+ "/state",
13
+ summary="Get App State",
14
+ description="Get the current application state, including configured chains and default settings.",
15
+ )
16
+ def get_state(auth: bool = Depends(verify_auth)):
17
+ """Get the current application state (configured chains, etc)."""
18
+ # Build native currencies map, tokens map, and collect chain names
19
+ chain_names = []
20
+ native_currencies = {}
21
+ tokens = {}
22
+ for name, interface in ChainInterfaces().items():
23
+ chain_names.append(name)
24
+ native_currencies[name] = interface.chain.native_currency
25
+ # Get token symbols from the interface (dict of symbol -> address)
26
+ tokens[name] = list(interface.tokens.keys())
27
+
28
+ return {
29
+ "chains": chain_names,
30
+ "tokens": tokens,
31
+ "native_currencies": native_currencies,
32
+ "default_chain": "gnosis",
33
+ }
34
+
35
+
36
+ def _obscure_url(url: str) -> str:
37
+ """Obscure API keys in URL."""
38
+ if any(param in url for param in ["api_key", "project_id", "key"]):
39
+ return url.split("?")[0] + "?***"
40
+ return url
41
+
42
+
43
+ @router.get(
44
+ "/rpc-status",
45
+ summary="Get RPC Status",
46
+ description="Check the connectivity and sync status of RPC endpoints for all chains.",
47
+ )
48
+ def get_rpc_status(auth: bool = Depends(verify_auth)):
49
+ """Get status of RPC endpoints."""
50
+ from iwa.core.chain import ChainInterfaces
51
+
52
+ status = {}
53
+ for name, interface in ChainInterfaces().items():
54
+ try:
55
+ # Simple check using block number
56
+ block = interface.web3.eth.block_number
57
+ rpcs = [_obscure_url(rpc) for rpc in interface.chain.rpcs]
58
+ status[name] = {"status": "online", "block": block, "rpcs": rpcs}
59
+ except Exception as e:
60
+ status[name] = {
61
+ "status": "offline",
62
+ "error": str(e),
63
+ "rpcs": [_obscure_url(rpc) for rpc in interface.chain.rpcs],
64
+ }
65
+ return status