iwa 0.0.1a2__py3-none-any.whl → 0.0.1a4__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 (67) hide show
  1. iwa/core/chain/interface.py +51 -61
  2. iwa/core/chain/models.py +7 -7
  3. iwa/core/chain/rate_limiter.py +21 -10
  4. iwa/core/cli.py +27 -2
  5. iwa/core/constants.py +6 -5
  6. iwa/core/contracts/abis/erc20.json +930 -0
  7. iwa/core/contracts/abis/multisend.json +24 -0
  8. iwa/core/contracts/abis/multisend_call_only.json +17 -0
  9. iwa/core/contracts/contract.py +16 -4
  10. iwa/core/ipfs.py +149 -0
  11. iwa/core/keys.py +259 -29
  12. iwa/core/mnemonic.py +3 -13
  13. iwa/core/models.py +28 -6
  14. iwa/core/pricing.py +4 -4
  15. iwa/core/secrets.py +77 -0
  16. iwa/core/services/safe.py +3 -3
  17. iwa/core/utils.py +6 -1
  18. iwa/core/wallet.py +4 -0
  19. iwa/plugins/gnosis/safe.py +2 -2
  20. iwa/plugins/gnosis/tests/test_safe.py +1 -1
  21. iwa/plugins/olas/constants.py +8 -0
  22. iwa/plugins/olas/contracts/abis/activity_checker.json +110 -0
  23. iwa/plugins/olas/contracts/abis/mech.json +740 -0
  24. iwa/plugins/olas/contracts/abis/mech_marketplace.json +1293 -0
  25. iwa/plugins/olas/contracts/abis/mech_new.json +954 -0
  26. iwa/plugins/olas/contracts/abis/service_manager.json +1382 -0
  27. iwa/plugins/olas/contracts/abis/service_registry.json +1909 -0
  28. iwa/plugins/olas/contracts/abis/staking.json +1400 -0
  29. iwa/plugins/olas/contracts/abis/staking_token.json +1274 -0
  30. iwa/plugins/olas/contracts/mech.py +30 -2
  31. iwa/plugins/olas/plugin.py +2 -2
  32. iwa/plugins/olas/tests/test_plugin_full.py +3 -3
  33. iwa/plugins/olas/tests/test_staking_integration.py +2 -2
  34. iwa/tools/__init__.py +1 -0
  35. iwa/tools/check_profile.py +6 -5
  36. iwa/tools/list_contracts.py +136 -0
  37. iwa/tools/release.py +9 -3
  38. iwa/tools/reset_env.py +2 -2
  39. iwa/tools/reset_tenderly.py +26 -24
  40. iwa/tools/wallet_check.py +150 -0
  41. iwa/web/dependencies.py +4 -4
  42. iwa/web/routers/state.py +1 -0
  43. iwa/web/static/app.js +3096 -0
  44. iwa/web/static/index.html +543 -0
  45. iwa/web/static/style.css +1443 -0
  46. iwa/web/tests/test_web_endpoints.py +3 -2
  47. iwa/web/tests/test_web_swap_coverage.py +156 -0
  48. {iwa-0.0.1a2.dist-info → iwa-0.0.1a4.dist-info}/METADATA +6 -3
  49. {iwa-0.0.1a2.dist-info → iwa-0.0.1a4.dist-info}/RECORD +64 -44
  50. iwa-0.0.1a4.dist-info/entry_points.txt +6 -0
  51. {iwa-0.0.1a2.dist-info → iwa-0.0.1a4.dist-info}/top_level.txt +0 -1
  52. tests/test_chain.py +1 -1
  53. tests/test_chain_interface_coverage.py +92 -0
  54. tests/test_contract.py +2 -0
  55. tests/test_keys.py +58 -15
  56. tests/test_migration.py +52 -0
  57. tests/test_mnemonic.py +1 -1
  58. tests/test_pricing.py +7 -7
  59. tests/test_safe_coverage.py +1 -1
  60. tests/test_safe_service.py +3 -3
  61. tests/test_staking_router.py +13 -1
  62. tools/verify_drain.py +1 -1
  63. conftest.py +0 -22
  64. iwa/core/settings.py +0 -95
  65. iwa-0.0.1a2.dist-info/entry_points.txt +0 -2
  66. {iwa-0.0.1a2.dist-info → iwa-0.0.1a4.dist-info}/WHEEL +0 -0
  67. {iwa-0.0.1a2.dist-info → iwa-0.0.1a4.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,9 @@
1
1
  """Mech contract interaction."""
2
2
 
3
- from typing import Dict, Optional
3
+ from typing import Any, Dict, Optional
4
4
 
5
5
  from iwa.core.contracts.contract import ContractInstance
6
+ from iwa.core.ipfs import metadata_to_request_data
6
7
  from iwa.core.types import EthereumAddress
7
8
  from iwa.plugins.olas.contracts.base import OLAS_ABI_PATH
8
9
 
@@ -38,7 +39,13 @@ class MechContract(ContractInstance):
38
39
  data: bytes,
39
40
  value: Optional[int] = None,
40
41
  ) -> Optional[Dict]:
41
- """Prepare a request transaction."""
42
+ """Prepare a request transaction from raw data bytes.
43
+
44
+ :param from_address: The address initiating the request.
45
+ :param data: The raw request data bytes (typically an IPFS hash).
46
+ :param value: The value to send with the transaction. Defaults to contract price.
47
+ :return: The prepared transaction dict, or None if preparation fails.
48
+ """
42
49
  if value is None:
43
50
  value = self.get_price()
44
51
 
@@ -47,3 +54,24 @@ class MechContract(ContractInstance):
47
54
  method_kwargs={"data": data},
48
55
  tx_params={"from": from_address, "value": value},
49
56
  )
57
+
58
+ def prepare_request_from_metadata(
59
+ self,
60
+ from_address: EthereumAddress,
61
+ metadata: Dict[str, Any],
62
+ value: Optional[int] = None,
63
+ ) -> Optional[Dict]:
64
+ """Prepare a mech request by pushing metadata to IPFS.
65
+
66
+ This method handles the full flow:
67
+ 1. Pushing the metadata JSON to IPFS
68
+ 2. Converting the IPFS hash to request data bytes
69
+ 3. Preparing the request transaction
70
+
71
+ :param from_address: The address initiating the request.
72
+ :param metadata: The metadata dict to push to IPFS (e.g., {"prompt": ..., "tool": ...}).
73
+ :param value: The value to send with the transaction. Defaults to contract price.
74
+ :return: The prepared transaction dict, or None if preparation fails.
75
+ """
76
+ request_data = metadata_to_request_data(metadata)
77
+ return self.prepare_request_tx(from_address, request_data, value)
@@ -65,9 +65,9 @@ class OlasPlugin(Plugin):
65
65
  from safe_eth.eth import EthereumClient
66
66
  from safe_eth.safe import Safe
67
67
 
68
- from iwa.core.settings import settings
68
+ from iwa.core.secrets import secrets
69
69
 
70
- rpc_secret = getattr(settings, f"{chain_name}_rpc", None)
70
+ rpc_secret = getattr(secrets, f"{chain_name}_rpc", None)
71
71
  if not rpc_secret:
72
72
  return None, None # Can't verify, skip
73
73
 
@@ -103,14 +103,14 @@ def test_import_services_cli_full(plugin, runner):
103
103
  def test_get_safe_signers_edge_cases(plugin):
104
104
  """Test _get_safe_signers with various failure scenarios."""
105
105
  # 1. No RPC configured
106
- with patch("iwa.core.settings.settings") as mock_settings:
106
+ with patch("iwa.core.secrets.secrets") as mock_settings:
107
107
  mock_settings.gnosis_rpc = None
108
108
  signers, exists = plugin._get_safe_signers("0x1", "gnosis")
109
109
  assert signers is None
110
110
  assert exists is None
111
111
 
112
112
  # 2. Safe doesn't exist (raises exception)
113
- with patch("iwa.core.settings.settings") as mock_settings:
113
+ with patch("iwa.core.secrets.secrets") as mock_settings:
114
114
  mock_settings.gnosis_rpc = MagicMock()
115
115
  with patch("safe_eth.eth.EthereumClient"), patch("safe_eth.safe.Safe") as mock_safe_cls:
116
116
  mock_safe = mock_safe_cls.return_value
@@ -121,7 +121,7 @@ def test_get_safe_signers_edge_cases(plugin):
121
121
  assert exists is False
122
122
 
123
123
  # 3. Success path
124
- with patch("iwa.core.settings.settings") as mock_settings:
124
+ with patch("iwa.core.secrets.secrets") as mock_settings:
125
125
  mock_settings.gnosis_rpc = MagicMock()
126
126
  with patch("safe_eth.eth.EthereumClient"), patch("safe_eth.safe.Safe") as mock_safe_cls:
127
127
  mock_safe = mock_safe_cls.return_value
@@ -247,8 +247,8 @@ def test_staking_contract(tmp_path): # noqa: C901
247
247
  assert staking.call("nonexistent") == 0
248
248
 
249
249
  # Activity checker interactions - nonces now returns [safe_nonce, mech_requests]
250
- mock_contract.functions.getMultisigNonces.return_value.call.return_value = [5, 3]
251
- staking.activity_checker.contract = mock_contract
250
+ # Mock via patch since contract is now a property
251
+ staking.activity_checker.get_multisig_nonces = MagicMock(return_value=(5, 3))
252
252
 
253
253
  staking.ts_checkpoint = MagicMock(return_value=0)
254
254
 
iwa/tools/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Tools for IWA."""
@@ -3,7 +3,8 @@
3
3
 
4
4
  import requests
5
5
 
6
- from iwa.core.settings import settings
6
+ from iwa.core.models import Config
7
+ from iwa.core.secrets import secrets
7
8
 
8
9
 
9
10
  def check_rpc_status(rpc_url):
@@ -16,8 +17,8 @@ def check_rpc_status(rpc_url):
16
17
 
17
18
  headers = {"Content-Type": "application/json"}
18
19
  # Include access key if present
19
- if settings.tenderly_access_key:
20
- headers["X-Access-Key"] = settings.tenderly_access_key.get_secret_value()
20
+ if secrets.tenderly_access_key:
21
+ headers["X-Access-Key"] = secrets.tenderly_access_key.get_secret_value()
21
22
 
22
23
  payload = {"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1}
23
24
 
@@ -56,10 +57,10 @@ def check_rpc_status(rpc_url):
56
57
 
57
58
  def main():
58
59
  """Check and display the active Tenderly profile status."""
59
- print(f"Active Tenderly Profile: {settings.tenderly_profile}")
60
+ print(f"Active Tenderly Profile: {Config().core.tenderly_profile}")
60
61
 
61
62
  # Check Gnosis RPC as primary indicator
62
- rpc = settings.gnosis_rpc.get_secret_value() if settings.gnosis_rpc else None
63
+ rpc = secrets.gnosis_rpc.get_secret_value() if secrets.gnosis_rpc else None
63
64
  check_rpc_status(rpc)
64
65
 
65
66
 
@@ -0,0 +1,136 @@
1
+ """Tool to list Olas staking contracts status."""
2
+
3
+ import argparse
4
+ import logging
5
+
6
+ from rich.console import Console
7
+ from rich.progress import track
8
+ from rich.table import Table
9
+
10
+ from iwa.core.utils import configure_logger
11
+ from iwa.plugins.olas.constants import OLAS_TRADER_STAKING_CONTRACTS
12
+ from iwa.plugins.olas.contracts.staking import StakingContract
13
+
14
+ # Configure logger to avoid noise during execution
15
+ logger = configure_logger()
16
+ logging.getLogger("web3").setLevel(logging.WARNING)
17
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
18
+
19
+
20
+ def parse_args():
21
+ """Parse command line arguments."""
22
+ parser = argparse.ArgumentParser(description="List Olas staking contracts.")
23
+ parser.add_argument(
24
+ "--sort",
25
+ type=str,
26
+ choices=["name", "rewards", "epoch", "slots", "olas"],
27
+ default="name",
28
+ help="Sort by field (default: name)",
29
+ )
30
+ return parser.parse_args()
31
+
32
+
33
+ def fetch_contract_data(chain_name):
34
+ """Fetch data for all contracts."""
35
+ contracts_map = OLAS_TRADER_STAKING_CONTRACTS.get(chain_name, {})
36
+ contract_data = []
37
+
38
+ for name, address in track(contracts_map.items(), description="Fetching contract data..."):
39
+ try:
40
+ contract = StakingContract(address, chain_name=chain_name)
41
+
42
+ # Needed Olas (Bond + Deposit)
43
+ needed_olas = (contract.min_staking_deposit * 2) / 1e18
44
+
45
+ # Slots
46
+ service_ids = contract.get_service_ids()
47
+ max_slots = contract.max_num_services
48
+
49
+ # Rewards & Balance
50
+ rewards_olas = contract.available_rewards / 1e18
51
+ balance_olas = contract.balance / 1e18
52
+
53
+ contract_data.append(
54
+ {
55
+ "name": name,
56
+ "needed_olas": needed_olas,
57
+ "occupied_slots": len(service_ids),
58
+ "max_slots": max_slots,
59
+ "free_slots": max_slots - len(service_ids),
60
+ "rewards_olas": rewards_olas,
61
+ "balance_olas": balance_olas,
62
+ "epoch_end": contract.get_next_epoch_start(),
63
+ "error": None,
64
+ }
65
+ )
66
+ except Exception as e:
67
+ contract_data.append({"name": name, "error": str(e)})
68
+
69
+ return contract_data
70
+
71
+
72
+ def sort_contract_data(contract_data, sort_criterion):
73
+ """Sort contract data based on criterion."""
74
+ if sort_criterion == "name":
75
+ contract_data.sort(key=lambda x: x["name"])
76
+ elif sort_criterion == "rewards":
77
+ contract_data.sort(
78
+ key=lambda x: (x.get("rewards_olas", -1) if not x.get("error") else -1), reverse=True
79
+ )
80
+ elif sort_criterion == "epoch":
81
+ safe_max = 32503680000
82
+ contract_data.sort(key=lambda x: safe_max if x.get("error") else x["epoch_end"].timestamp())
83
+ elif sort_criterion == "slots":
84
+ contract_data.sort(
85
+ key=lambda x: (x.get("free_slots", -1) if not x.get("error") else -1), reverse=True
86
+ )
87
+ elif sort_criterion == "olas":
88
+ contract_data.sort(
89
+ key=lambda x: (
90
+ x.get("needed_olas", float("inf")) if not x.get("error") else float("inf")
91
+ )
92
+ )
93
+
94
+
95
+ def print_table(console, contract_data, chain_name, sort_criterion):
96
+ """Print the contracts table."""
97
+ table = Table(title=f"Olas Staking Contracts ({chain_name}) - Sorted by: {sort_criterion}")
98
+ table.add_column("Contract Name", style="cyan", no_wrap=True)
99
+ table.add_column("Necessary Olas", justify="right", style="green")
100
+ table.add_column("Slots (Free/Max)", justify="right", style="magenta")
101
+ table.add_column("Available Rewards", justify="right", style="yellow")
102
+ table.add_column("Contract Balance", justify="right", style="blue")
103
+ table.add_column("Epoch End (UTC)", justify="right", style="white")
104
+
105
+ for item in contract_data:
106
+ if item.get("error"):
107
+ table.add_row(item["name"], "ERROR", "-", "-", "-", item["error"])
108
+ else:
109
+ table.add_row(
110
+ item["name"],
111
+ f"{item['needed_olas']:,.0f} OLAS",
112
+ f"{item['free_slots']}/{item['max_slots']}",
113
+ f"{item['rewards_olas']:,.2f} OLAS",
114
+ f"{item['balance_olas']:,.2f} OLAS",
115
+ item["epoch_end"].strftime("%Y-%m-%d %H:%M:%S"),
116
+ )
117
+ console.print(table)
118
+
119
+
120
+ def main():
121
+ """Run the contracts list tool."""
122
+ args = parse_args()
123
+ console = Console()
124
+ chain_name = "gnosis"
125
+
126
+ if chain_name not in OLAS_TRADER_STAKING_CONTRACTS:
127
+ console.print(f"[red]No contracts found for chain {chain_name}[/red]")
128
+ return
129
+
130
+ contract_data = fetch_contract_data(chain_name)
131
+ sort_contract_data(contract_data, args.sort)
132
+ print_table(console, contract_data, chain_name, args.sort)
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()
iwa/tools/release.py CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  Usage: python release.py <version>
5
5
  """
6
+
6
7
  import argparse
7
8
  import subprocess # nosec: B404
8
9
  import sys
@@ -18,7 +19,7 @@ def run(cmd: str, check: bool = True, capture: bool = False) -> str:
18
19
  check=check,
19
20
  text=True,
20
21
  stdout=subprocess.PIPE if capture else None,
21
- stderr=subprocess.PIPE if capture else None
22
+ stderr=subprocess.PIPE if capture else None,
22
23
  )
23
24
  return result.stdout.strip() if capture else ""
24
25
  except subprocess.CalledProcessError as e:
@@ -26,27 +27,31 @@ def run(cmd: str, check: bool = True, capture: bool = False) -> str:
26
27
  print(f"Error output: {e.stderr}")
27
28
  sys.exit(e.returncode)
28
29
 
30
+
29
31
  def error(msg: str) -> NoReturn:
30
32
  """Print error and exit."""
31
33
  print(f"❌ Error: {msg}")
32
34
  sys.exit(1)
33
35
 
36
+
34
37
  def info(msg: str) -> None:
35
38
  """Print info message."""
36
39
  print(f"🚀 {msg}")
37
40
 
41
+
38
42
  def confirm(question: str) -> bool:
39
43
  """Ask for user confirmation."""
40
44
  while True:
41
45
  try:
42
46
  choice = input(f"{question} [y/N] ").lower()
43
- if not choice or choice == 'n':
47
+ if not choice or choice == "n":
44
48
  return False
45
- if choice == 'y':
49
+ if choice == "y":
46
50
  return True
47
51
  except EOFError:
48
52
  return False
49
53
 
54
+
50
55
  def main() -> None:
51
56
  """Execute the release process."""
52
57
  parser = argparse.ArgumentParser(description="Create a new release")
@@ -107,5 +112,6 @@ def main() -> None:
107
112
  run(f"git push {check_url} {tag}")
108
113
  print(f"✅ Release {tag} triggered! Check GitHub Actions.")
109
114
 
115
+
110
116
  if __name__ == "__main__":
111
117
  main()
iwa/tools/reset_env.py CHANGED
@@ -13,7 +13,7 @@ import sys
13
13
  import yaml
14
14
 
15
15
  from iwa.core.constants import CONFIG_PATH, WALLET_PATH
16
- from iwa.core.settings import settings
16
+ from iwa.core.models import Config
17
17
 
18
18
 
19
19
  def _reset_tenderly(profile: int) -> None:
@@ -97,7 +97,7 @@ def _clean_wallet_accounts() -> None:
97
97
 
98
98
  def main():
99
99
  """Reset the environment by clearing networks, services, and accounts."""
100
- profile = settings.tenderly_profile
100
+ profile = Config().core.tenderly_profile
101
101
  print(f"Detected Tenderly profile: {profile}")
102
102
 
103
103
  _reset_tenderly(profile)
@@ -10,25 +10,25 @@ import sys
10
10
  from typing import List, Optional, Tuple
11
11
 
12
12
  import requests
13
+ from dotenv import load_dotenv
13
14
  from web3 import Web3
14
15
 
15
16
  from iwa.core.constants import SECRETS_PATH, get_tenderly_config_path
16
17
  from iwa.core.keys import KeyStorage
17
- from iwa.core.models import TenderlyConfig
18
- from iwa.core.settings import settings
18
+ from iwa.core.models import Config, TenderlyConfig
19
19
 
20
+ # Load secrets.env for local development
21
+ if SECRETS_PATH.exists():
22
+ load_dotenv(SECRETS_PATH, override=True)
20
23
 
21
- def get_tenderly_credentials() -> Tuple[Optional[str], Optional[str], Optional[str]]:
22
- """Get Tenderly credentials from settings (based on current profile)."""
23
- return (
24
- settings.tenderly_account_slug.get_secret_value()
25
- if settings.tenderly_account_slug
26
- else None,
27
- settings.tenderly_project_slug.get_secret_value()
28
- if settings.tenderly_project_slug
29
- else None,
30
- settings.tenderly_access_key.get_secret_value() if settings.tenderly_access_key else None,
31
- )
24
+
25
+ def get_tenderly_credentials(profile: int) -> Tuple[Optional[str], Optional[str], Optional[str]]:
26
+ """Get Tenderly credentials dynamically based on profile."""
27
+ # Secrets are still in env, keyed by profile
28
+ account = os.getenv(f"tenderly_account_slug_{profile}")
29
+ project = os.getenv(f"tenderly_project_slug_{profile}")
30
+ access_key = os.getenv(f"tenderly_access_key_{profile}")
31
+ return account, project, access_key
32
32
 
33
33
 
34
34
  def _generate_default_config() -> TenderlyConfig:
@@ -38,6 +38,7 @@ def _generate_default_config() -> TenderlyConfig:
38
38
 
39
39
  vnets = {}
40
40
  chains = SupportedChains()
41
+ config = Config()
41
42
 
42
43
  for chain_name, chain in [
43
44
  ("gnosis", chains.gnosis),
@@ -53,7 +54,7 @@ def _generate_default_config() -> TenderlyConfig:
53
54
  TokenAmount(
54
55
  address=str(olas_address),
55
56
  symbol="OLAS",
56
- amount_eth=settings.tenderly_olas_funds,
57
+ amount_eth=config.core.tenderly_olas_funds,
57
58
  )
58
59
  )
59
60
 
@@ -61,7 +62,7 @@ def _generate_default_config() -> TenderlyConfig:
61
62
  chain_id=chain.chain_id,
62
63
  funds_requirements={
63
64
  "all": FundRequirements(
64
- native_eth=settings.tenderly_native_funds,
65
+ native_eth=config.core.tenderly_native_funds,
65
66
  tokens=tokens,
66
67
  )
67
68
  },
@@ -295,11 +296,12 @@ def _fund_vnet_accounts(vnet, keys) -> None:
295
296
 
296
297
 
297
298
  def main() -> None:
298
- """Main - uses tenderly_profile from settings."""
299
- profile = settings.tenderly_profile
299
+ """Main - uses tenderly_profile from Config."""
300
+ config = Config()
301
+ profile = config.core.tenderly_profile
300
302
  print(f"Recreating Tenderly Networks (Profile {profile})")
301
303
 
302
- account_slug, project_slug, tenderly_access_key = get_tenderly_credentials()
304
+ account_slug, project_slug, tenderly_access_key = get_tenderly_credentials(profile)
303
305
 
304
306
  if not account_slug or not project_slug or not tenderly_access_key:
305
307
  print(f"Missing Tenderly environment variables for profile {profile}")
@@ -350,13 +352,13 @@ if __name__ == "__main__": # pragma: no cover
350
352
  os.environ["TENDERLY_PROFILE"] = str(args.profile)
351
353
 
352
354
  # Reset the singleton to reload with new env
353
- from iwa.core.settings import Settings
355
+ from iwa.core.secrets import Secrets
354
356
 
355
- Settings._instance = None # type: ignore
357
+ Secrets._instance = None # type: ignore
356
358
 
357
- # Reimport settings to get fresh instance
358
- if "iwa.core.settings" in sys.modules:
359
- del sys.modules["iwa.core.settings"]
360
- from iwa.core.settings import settings # noqa: F401
359
+ # Reimport secrets to get fresh instance
360
+ if "iwa.core.secrets" in sys.modules:
361
+ del sys.modules["iwa.core.secrets"]
362
+ from iwa.core.secrets import secrets # noqa: F401
361
363
 
362
364
  main()
@@ -0,0 +1,150 @@
1
+ """Wallet integrity check utility."""
2
+
3
+ import sys
4
+
5
+ from eth_account import Account
6
+
7
+ from iwa.core.keys import KeyStorage
8
+ from iwa.core.mnemonic import EncryptedMnemonic
9
+ from iwa.core.models import StoredSafeAccount
10
+ from iwa.core.secrets import secrets
11
+ from iwa.core.utils import configure_logger
12
+
13
+ # Configure logger to be quiet for this tool unless error
14
+ logger = configure_logger()
15
+
16
+
17
+ def _check_accounts(storage: KeyStorage) -> bool:
18
+ """Verify that all EOAs in the wallet can be decrypted.
19
+
20
+ Args:
21
+ storage: The KeyStorage instance.
22
+
23
+ Returns:
24
+ bool: True if all EOAs were verified successfully.
25
+
26
+ """
27
+ if not storage.accounts:
28
+ print("⚠️ No accounts found in wallet.json.")
29
+ return True
30
+
31
+ success_count = 0
32
+ fail_count = 0
33
+ safe_count = 0
34
+
35
+ # Ensure we sort by tag for consistent output
36
+ sorted_accounts = sorted(storage.accounts.values(), key=lambda x: x.tag if x.tag else "")
37
+
38
+ for account in sorted_accounts:
39
+ if isinstance(account, StoredSafeAccount):
40
+ print(
41
+ f"🔹 [Safe] {account.address} (tag: {account.tag or 'none'}) "
42
+ "- Skipped (Contract Wallet)"
43
+ )
44
+ safe_count += 1
45
+ continue
46
+
47
+ try:
48
+ # Decrypt using the password from secrets.env
49
+ priv_key = account.decrypt_private_key()
50
+
51
+ # Verify address matches
52
+ derived_acct = Account.from_key(priv_key)
53
+
54
+ if derived_acct.address.lower() == account.address.lower():
55
+ print(f"✅ [EOA] {account.address} (tag: {account.tag or 'none'}) - OK")
56
+ success_count += 1
57
+ else:
58
+ print(
59
+ f"❌ [EOA] {account.address} (tag: {account.tag or 'none'}) "
60
+ "- ADDRESS MISMATCH!"
61
+ )
62
+ print(f" Expected: {account.address}")
63
+ print(f" Derived: {derived_acct.address}")
64
+ fail_count += 1
65
+ except Exception as e:
66
+ print(
67
+ f"❌ [EOA] {account.address} (tag: {account.tag or 'none'}) - DECRYPTION FAILED!"
68
+ )
69
+ print(f" Error: {e}")
70
+ fail_count += 1
71
+
72
+ print("\n" + "-" * 40)
73
+ print(f"Accounts Verified: {success_count}")
74
+ print(f"Accounts Failed: {fail_count}")
75
+ if safe_count:
76
+ print(f"Safes Skipped: {safe_count}")
77
+
78
+ return fail_count == 0
79
+
80
+
81
+ def _check_mnemonic(storage: KeyStorage) -> bool:
82
+ """Verify that the encrypted mnemonic can be decrypted.
83
+
84
+ Args:
85
+ storage: The KeyStorage instance.
86
+
87
+ Returns:
88
+ bool: True if the mnemonic was verified successfully.
89
+
90
+ """
91
+ print("\n🔍 Checking Mnemonic...")
92
+ if not storage.encrypted_mnemonic:
93
+ print("⚠️ No encrypted mnemonic found in wallet.json.")
94
+ return True
95
+
96
+ try:
97
+ # Instantiate EncryptedMnemonic from the dict stored in KeyStorage
98
+ enc_mnemonic = EncryptedMnemonic(**storage.encrypted_mnemonic)
99
+
100
+ # Get password (checked implicitly by KeyStorage init)
101
+ password = secrets.wallet_password.get_secret_value()
102
+
103
+ # Attempt decryption
104
+ mnemonic_text = enc_mnemonic.decrypt(password)
105
+
106
+ if mnemonic_text:
107
+ # Basic validation (e.g. check word count) - explicit
108
+ word_count = len(mnemonic_text.split())
109
+ if word_count in [12, 15, 18, 21, 24]:
110
+ print(f"✅ [Mnemonic] Decryption successful ({word_count} words).")
111
+ return True
112
+ print(f"⚠️ [Mnemonic] Decryption successful but unusual word count: {word_count}")
113
+ return True
114
+
115
+ print("❌ [Mnemonic] Decrypted to empty string.")
116
+ return False
117
+
118
+ except Exception as e:
119
+ print(f"❌ [Mnemonic] Decryption FAILED! Error: {e}")
120
+ return False
121
+
122
+
123
+ def check_wallet() -> None:
124
+ """Verify that all EOAs in the wallet can be decrypted and mnemonic is valid."""
125
+ print("🔍 Verifying wallet integrity...")
126
+ print("This process checks if the WALLET_PASSWORD in secrets.env can decrypt all accounts.")
127
+ print()
128
+
129
+ try:
130
+ # KeyStorage loads WALLET_PATH and uses secrets.wallet_password by default
131
+ storage = KeyStorage()
132
+ except Exception as e:
133
+ print(f"❌ Critical Error: Could not initialize KeyStorage. {e}")
134
+ sys.exit(1)
135
+
136
+ accounts_ok = _check_accounts(storage)
137
+ mnemonic_ok = _check_mnemonic(storage)
138
+
139
+ print("\n" + "=" * 40)
140
+ print("REPORT SUMMARY")
141
+ if accounts_ok and mnemonic_ok:
142
+ print("✨ All checks passed! Wallet is healthy.")
143
+ sys.exit(0)
144
+ else:
145
+ print("❌ Wallet check FAILED. See errors above.")
146
+ sys.exit(1)
147
+
148
+
149
+ if __name__ == "__main__":
150
+ check_wallet()
iwa/web/dependencies.py CHANGED
@@ -19,11 +19,11 @@ api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
19
19
 
20
20
 
21
21
  def _get_webui_password() -> Optional[str]:
22
- """Get WEBUI_PASSWORD from settings (lazy load to ensure secrets.env is loaded)."""
23
- from iwa.core.settings import settings
22
+ """Get WEBUI_PASSWORD from secrets (lazy load to ensure secrets.env is loaded)."""
23
+ from iwa.core.secrets import secrets
24
24
 
25
- if hasattr(settings, "webui_password") and settings.webui_password:
26
- return settings.webui_password.get_secret_value()
25
+ if hasattr(secrets, "webui_password") and secrets.webui_password:
26
+ return secrets.webui_password.get_secret_value()
27
27
  return None
28
28
 
29
29
 
iwa/web/routers/state.py CHANGED
@@ -30,6 +30,7 @@ def get_state(auth: bool = Depends(verify_auth)):
30
30
  "tokens": tokens,
31
31
  "native_currencies": native_currencies,
32
32
  "default_chain": "gnosis",
33
+ "testing": ChainInterfaces().gnosis.is_tenderly,
33
34
  }
34
35
 
35
36