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.
- iwa/core/chain/interface.py +51 -61
- iwa/core/chain/models.py +7 -7
- iwa/core/chain/rate_limiter.py +21 -10
- iwa/core/cli.py +27 -2
- iwa/core/constants.py +6 -5
- iwa/core/contracts/abis/erc20.json +930 -0
- iwa/core/contracts/abis/multisend.json +24 -0
- iwa/core/contracts/abis/multisend_call_only.json +17 -0
- iwa/core/contracts/contract.py +16 -4
- iwa/core/ipfs.py +149 -0
- iwa/core/keys.py +259 -29
- iwa/core/mnemonic.py +3 -13
- iwa/core/models.py +28 -6
- iwa/core/pricing.py +4 -4
- iwa/core/secrets.py +77 -0
- iwa/core/services/safe.py +3 -3
- iwa/core/utils.py +6 -1
- iwa/core/wallet.py +4 -0
- iwa/plugins/gnosis/safe.py +2 -2
- iwa/plugins/gnosis/tests/test_safe.py +1 -1
- iwa/plugins/olas/constants.py +8 -0
- iwa/plugins/olas/contracts/abis/activity_checker.json +110 -0
- iwa/plugins/olas/contracts/abis/mech.json +740 -0
- iwa/plugins/olas/contracts/abis/mech_marketplace.json +1293 -0
- iwa/plugins/olas/contracts/abis/mech_new.json +954 -0
- iwa/plugins/olas/contracts/abis/service_manager.json +1382 -0
- iwa/plugins/olas/contracts/abis/service_registry.json +1909 -0
- iwa/plugins/olas/contracts/abis/staking.json +1400 -0
- iwa/plugins/olas/contracts/abis/staking_token.json +1274 -0
- iwa/plugins/olas/contracts/mech.py +30 -2
- iwa/plugins/olas/plugin.py +2 -2
- iwa/plugins/olas/tests/test_plugin_full.py +3 -3
- iwa/plugins/olas/tests/test_staking_integration.py +2 -2
- iwa/tools/__init__.py +1 -0
- iwa/tools/check_profile.py +6 -5
- iwa/tools/list_contracts.py +136 -0
- iwa/tools/release.py +9 -3
- iwa/tools/reset_env.py +2 -2
- iwa/tools/reset_tenderly.py +26 -24
- iwa/tools/wallet_check.py +150 -0
- iwa/web/dependencies.py +4 -4
- iwa/web/routers/state.py +1 -0
- iwa/web/static/app.js +3096 -0
- iwa/web/static/index.html +543 -0
- iwa/web/static/style.css +1443 -0
- iwa/web/tests/test_web_endpoints.py +3 -2
- iwa/web/tests/test_web_swap_coverage.py +156 -0
- {iwa-0.0.1a2.dist-info → iwa-0.0.1a4.dist-info}/METADATA +6 -3
- {iwa-0.0.1a2.dist-info → iwa-0.0.1a4.dist-info}/RECORD +64 -44
- iwa-0.0.1a4.dist-info/entry_points.txt +6 -0
- {iwa-0.0.1a2.dist-info → iwa-0.0.1a4.dist-info}/top_level.txt +0 -1
- tests/test_chain.py +1 -1
- tests/test_chain_interface_coverage.py +92 -0
- tests/test_contract.py +2 -0
- tests/test_keys.py +58 -15
- tests/test_migration.py +52 -0
- tests/test_mnemonic.py +1 -1
- tests/test_pricing.py +7 -7
- tests/test_safe_coverage.py +1 -1
- tests/test_safe_service.py +3 -3
- tests/test_staking_router.py +13 -1
- tools/verify_drain.py +1 -1
- conftest.py +0 -22
- iwa/core/settings.py +0 -95
- iwa-0.0.1a2.dist-info/entry_points.txt +0 -2
- {iwa-0.0.1a2.dist-info → iwa-0.0.1a4.dist-info}/WHEEL +0 -0
- {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)
|
iwa/plugins/olas/plugin.py
CHANGED
|
@@ -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.
|
|
68
|
+
from iwa.core.secrets import secrets
|
|
69
69
|
|
|
70
|
-
rpc_secret = getattr(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
251
|
-
staking.activity_checker.
|
|
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."""
|
iwa/tools/check_profile.py
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import requests
|
|
5
5
|
|
|
6
|
-
from iwa.core.
|
|
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
|
|
20
|
-
headers["X-Access-Key"] =
|
|
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: {
|
|
60
|
+
print(f"Active Tenderly Profile: {Config().core.tenderly_profile}")
|
|
60
61
|
|
|
61
62
|
# Check Gnosis RPC as primary indicator
|
|
62
|
-
rpc =
|
|
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 ==
|
|
47
|
+
if not choice or choice == "n":
|
|
44
48
|
return False
|
|
45
|
-
if choice ==
|
|
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.
|
|
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 =
|
|
100
|
+
profile = Config().core.tenderly_profile
|
|
101
101
|
print(f"Detected Tenderly profile: {profile}")
|
|
102
102
|
|
|
103
103
|
_reset_tenderly(profile)
|
iwa/tools/reset_tenderly.py
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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=
|
|
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=
|
|
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
|
|
299
|
-
|
|
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.
|
|
355
|
+
from iwa.core.secrets import Secrets
|
|
354
356
|
|
|
355
|
-
|
|
357
|
+
Secrets._instance = None # type: ignore
|
|
356
358
|
|
|
357
|
-
# Reimport
|
|
358
|
-
if "iwa.core.
|
|
359
|
-
del sys.modules["iwa.core.
|
|
360
|
-
from iwa.core.
|
|
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
|
|
23
|
-
from iwa.core.
|
|
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(
|
|
26
|
-
return
|
|
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
|
|