iwa 0.0.0__py3-none-any.whl → 0.0.1a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tool to check the active Tenderly profile."""
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from iwa.core.settings import settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_rpc_status(rpc_url):
|
|
10
|
+
"""Check the status of an RPC endpoint."""
|
|
11
|
+
if not rpc_url:
|
|
12
|
+
print(" [!] No RPC URL found in secrets.env for this profile.")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
print(f" Checking RPC: {rpc_url}")
|
|
16
|
+
|
|
17
|
+
headers = {"Content-Type": "application/json"}
|
|
18
|
+
# Include access key if present
|
|
19
|
+
if settings.tenderly_access_key:
|
|
20
|
+
headers["X-Access-Key"] = settings.tenderly_access_key.get_secret_value()
|
|
21
|
+
|
|
22
|
+
payload = {"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1}
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
response = requests.post(rpc_url, json=payload, headers=headers, timeout=5)
|
|
26
|
+
|
|
27
|
+
# Check for HTTP errors (like 403)
|
|
28
|
+
if response.status_code != 200:
|
|
29
|
+
print(f" [X] HTTP ERROR: {response.status_code}")
|
|
30
|
+
try:
|
|
31
|
+
err = response.json()
|
|
32
|
+
if "error" in err:
|
|
33
|
+
print(f" Details: {err['error']}")
|
|
34
|
+
except Exception:
|
|
35
|
+
print(f" Response text: {response.text}")
|
|
36
|
+
|
|
37
|
+
if response.status_code == 403:
|
|
38
|
+
print(" => LIKELY QUOTA EXCEEDED (Rate Limit).")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Check for JSON-RPC errors (like -32004)
|
|
42
|
+
data = response.json()
|
|
43
|
+
if "error" in data:
|
|
44
|
+
code = data["error"].get("code")
|
|
45
|
+
msg = data["error"].get("message")
|
|
46
|
+
print(f" [X] JSON-RPC ERROR: {code} - {msg}")
|
|
47
|
+
if code == -32004:
|
|
48
|
+
print(" => QUOTA LIMIT REACHED.")
|
|
49
|
+
else:
|
|
50
|
+
block = int(data["result"], 16)
|
|
51
|
+
print(f" [OK] API Operational. Block: {block}")
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f" [!] Exception checking RPC: {e}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
"""Check and display the active Tenderly profile status."""
|
|
59
|
+
print(f"Active Tenderly Profile: {settings.tenderly_profile}")
|
|
60
|
+
|
|
61
|
+
# Check Gnosis RPC as primary indicator
|
|
62
|
+
rpc = settings.gnosis_rpc.get_secret_value() if settings.gnosis_rpc else None
|
|
63
|
+
check_rpc_status(rpc)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
main()
|
iwa/tools/release.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Release helper script.
|
|
3
|
+
|
|
4
|
+
Usage: python release.py <version>
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import subprocess # nosec: B404
|
|
8
|
+
import sys
|
|
9
|
+
from typing import NoReturn
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(cmd: str, check: bool = True, capture: bool = False) -> str:
|
|
13
|
+
"""Run a shell command."""
|
|
14
|
+
try:
|
|
15
|
+
result = subprocess.run(
|
|
16
|
+
cmd,
|
|
17
|
+
shell=True, # nosec: B602
|
|
18
|
+
check=check,
|
|
19
|
+
text=True,
|
|
20
|
+
stdout=subprocess.PIPE if capture else None,
|
|
21
|
+
stderr=subprocess.PIPE if capture else None
|
|
22
|
+
)
|
|
23
|
+
return result.stdout.strip() if capture else ""
|
|
24
|
+
except subprocess.CalledProcessError as e:
|
|
25
|
+
if capture:
|
|
26
|
+
print(f"Error output: {e.stderr}")
|
|
27
|
+
sys.exit(e.returncode)
|
|
28
|
+
|
|
29
|
+
def error(msg: str) -> NoReturn:
|
|
30
|
+
"""Print error and exit."""
|
|
31
|
+
print(f"❌ Error: {msg}")
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
def info(msg: str) -> None:
|
|
35
|
+
"""Print info message."""
|
|
36
|
+
print(f"🚀 {msg}")
|
|
37
|
+
|
|
38
|
+
def confirm(question: str) -> bool:
|
|
39
|
+
"""Ask for user confirmation."""
|
|
40
|
+
while True:
|
|
41
|
+
try:
|
|
42
|
+
choice = input(f"{question} [y/N] ").lower()
|
|
43
|
+
if not choice or choice == 'n':
|
|
44
|
+
return False
|
|
45
|
+
if choice == 'y':
|
|
46
|
+
return True
|
|
47
|
+
except EOFError:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def main() -> None:
|
|
51
|
+
"""Execute the release process."""
|
|
52
|
+
parser = argparse.ArgumentParser(description="Create a new release")
|
|
53
|
+
parser.add_argument("version", help="Version tag (e.g., 0.1.0)")
|
|
54
|
+
args = parser.parse_args()
|
|
55
|
+
|
|
56
|
+
version = args.version
|
|
57
|
+
if version.startswith("v"):
|
|
58
|
+
version = version[1:]
|
|
59
|
+
tag = f"v{version}"
|
|
60
|
+
|
|
61
|
+
# 1. Check git status
|
|
62
|
+
print("Checking git status...")
|
|
63
|
+
status = run("git status --porcelain", capture=True)
|
|
64
|
+
if status:
|
|
65
|
+
error("Working directory is not clean. Commit changes first.")
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# User requested SSH agent usage. If origin is HTTPS, we try to use SSH for the check explicitly.
|
|
69
|
+
# This bypasses the HTTPS auth prompt if the user has SSH keys configured.
|
|
70
|
+
origin_url = run("git remote get-url origin", capture=True)
|
|
71
|
+
check_url = origin_url
|
|
72
|
+
if origin_url.startswith("https://github.com/"):
|
|
73
|
+
# Convert https://github.com/user/repo.git -> git@github.com:user/repo.git
|
|
74
|
+
ssh_url = origin_url.replace("https://github.com/", "git@github.com:")
|
|
75
|
+
check_url = ssh_url
|
|
76
|
+
print(f"checking remote tags via SSH ({ssh_url})...")
|
|
77
|
+
|
|
78
|
+
# Use ls-remote on the specific URL (SSH) to check tags without prompting for HTTPS creds
|
|
79
|
+
exists_remotely = run(f"git ls-remote --tags {check_url} {tag}", check=False, capture=True)
|
|
80
|
+
|
|
81
|
+
except Exception:
|
|
82
|
+
# fetch failed (likely auth), assume we can proceed to push (which might prompt or work if user is right)
|
|
83
|
+
print("⚠️ Could not fetch remote info (auth needed?). Assuming tag is new remotely.")
|
|
84
|
+
exists_remotely = ""
|
|
85
|
+
|
|
86
|
+
# 3. Check if tag exists
|
|
87
|
+
exists_locally = run(f"git rev-parse {tag}", check=False, capture=True)
|
|
88
|
+
|
|
89
|
+
if exists_locally or exists_remotely:
|
|
90
|
+
print(f"⚠️ Tag {tag} already exists!")
|
|
91
|
+
if not confirm("Do you want to FORCE update it (delete and overwrite)?"):
|
|
92
|
+
error("Aborted by user.")
|
|
93
|
+
|
|
94
|
+
info(f"Force updating {tag}...")
|
|
95
|
+
run(f"git tag -f {tag}")
|
|
96
|
+
# Push to the explicit URL (likely SSH) to avoid HTTPS prompts
|
|
97
|
+
run(f"git push -f {check_url} {tag}")
|
|
98
|
+
print(f"✅ Tag {tag} force updated. GitHub Actions triggered.")
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
info(f"Preparing to release {tag}...")
|
|
102
|
+
if not confirm("Are you sure? This will trigger a deployment to PyPI and DockerHub"):
|
|
103
|
+
error("Aborted by user.")
|
|
104
|
+
|
|
105
|
+
run(f"git tag {tag}")
|
|
106
|
+
# Push to the explicit URL (likely SSH) to avoid HTTPS prompts
|
|
107
|
+
run(f"git push {check_url} {tag}")
|
|
108
|
+
print(f"✅ Release {tag} triggered! Check GitHub Actions.")
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
main()
|
iwa/tools/reset_env.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tool to reset the full environment.
|
|
3
|
+
|
|
4
|
+
1. Resets Tenderly networks (based on active profile).
|
|
5
|
+
2. Clears Olas services from config.yaml.
|
|
6
|
+
3. Clears all accounts from wallet.json except 'master'.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import subprocess # nosec B404
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from iwa.core.constants import CONFIG_PATH, WALLET_PATH
|
|
16
|
+
from iwa.core.settings import settings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _reset_tenderly(profile: int) -> None:
|
|
20
|
+
"""Reset Tenderly networks using just command."""
|
|
21
|
+
cmd = ["just", "reset-tenderly", str(profile)]
|
|
22
|
+
print(f"Running: {' '.join(cmd)}")
|
|
23
|
+
try:
|
|
24
|
+
subprocess.check_call(cmd) # nosec B603
|
|
25
|
+
except subprocess.CalledProcessError as e:
|
|
26
|
+
print(f"Error running reset-tenderly: {e}")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _clean_olas_services() -> None:
|
|
31
|
+
"""Remove all Olas services from config.yaml."""
|
|
32
|
+
if not CONFIG_PATH.exists():
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with open(CONFIG_PATH, "r") as f:
|
|
37
|
+
config = yaml.safe_load(f) or {}
|
|
38
|
+
|
|
39
|
+
if "plugins" not in config or "olas" not in config["plugins"]:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if "services" not in config["plugins"]["olas"]:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
services = config["plugins"]["olas"]["services"]
|
|
46
|
+
if not services:
|
|
47
|
+
print("No Olas services found in config.yaml.")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
print(f"Removing {len(services)} Olas services from config.yaml...")
|
|
51
|
+
config["plugins"]["olas"]["services"] = {}
|
|
52
|
+
with open(CONFIG_PATH, "w") as f:
|
|
53
|
+
yaml.dump(config, f)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"Error cleaning config.yaml: {e}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _clean_wallet_accounts() -> None:
|
|
59
|
+
"""Remove all accounts from wallet.json except master."""
|
|
60
|
+
if not WALLET_PATH.exists():
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
with open(WALLET_PATH, "r") as f:
|
|
65
|
+
data = json.load(f)
|
|
66
|
+
|
|
67
|
+
accounts = data.get("accounts", {})
|
|
68
|
+
|
|
69
|
+
# Find master account
|
|
70
|
+
master_addr = None
|
|
71
|
+
master_acct = None
|
|
72
|
+
for addr, acct in accounts.items():
|
|
73
|
+
if acct.get("tag") == "master":
|
|
74
|
+
master_addr = addr
|
|
75
|
+
master_acct = acct
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
if not master_addr:
|
|
79
|
+
print(
|
|
80
|
+
"Warning: Master account not found in wallet.json! Skipping cleanup to avoid data loss."
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if len(accounts) <= 1:
|
|
85
|
+
print("Only master account exists in wallet.json.")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
print(
|
|
89
|
+
f"Preserving master account ({master_addr}), removing {len(accounts) - 1} other accounts..."
|
|
90
|
+
)
|
|
91
|
+
data["accounts"] = {master_addr: master_acct}
|
|
92
|
+
with open(WALLET_PATH, "w") as f:
|
|
93
|
+
json.dump(data, f, indent=4)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Error cleaning wallet.json: {e}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main():
|
|
99
|
+
"""Reset the environment by clearing networks, services, and accounts."""
|
|
100
|
+
profile = settings.tenderly_profile
|
|
101
|
+
print(f"Detected Tenderly profile: {profile}")
|
|
102
|
+
|
|
103
|
+
_reset_tenderly(profile)
|
|
104
|
+
_clean_olas_services()
|
|
105
|
+
_clean_wallet_accounts()
|
|
106
|
+
|
|
107
|
+
print("Environment reset complete.")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
main()
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Recreates Tenderly networks and funds wallets as per configuration."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import random
|
|
7
|
+
import re
|
|
8
|
+
import string
|
|
9
|
+
import sys
|
|
10
|
+
from typing import List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from web3 import Web3
|
|
14
|
+
|
|
15
|
+
from iwa.core.constants import SECRETS_PATH, get_tenderly_config_path
|
|
16
|
+
from iwa.core.keys import KeyStorage
|
|
17
|
+
from iwa.core.models import TenderlyConfig
|
|
18
|
+
from iwa.core.settings import settings
|
|
19
|
+
|
|
20
|
+
|
|
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
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _generate_default_config() -> TenderlyConfig:
|
|
35
|
+
"""Generate a default TenderlyConfig from SupportedChains."""
|
|
36
|
+
from iwa.core.chain import SupportedChains
|
|
37
|
+
from iwa.core.models import FundRequirements, TokenAmount, VirtualNet
|
|
38
|
+
|
|
39
|
+
vnets = {}
|
|
40
|
+
chains = SupportedChains()
|
|
41
|
+
|
|
42
|
+
for chain_name, chain in [
|
|
43
|
+
("gnosis", chains.gnosis),
|
|
44
|
+
("ethereum", chains.ethereum),
|
|
45
|
+
("base", chains.base),
|
|
46
|
+
]:
|
|
47
|
+
# Get OLAS token address for this chain
|
|
48
|
+
olas_address = chain.tokens.get("OLAS")
|
|
49
|
+
|
|
50
|
+
tokens = []
|
|
51
|
+
if olas_address:
|
|
52
|
+
tokens.append(
|
|
53
|
+
TokenAmount(
|
|
54
|
+
address=str(olas_address),
|
|
55
|
+
symbol="OLAS",
|
|
56
|
+
amount_eth=settings.tenderly_olas_funds,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
vnets[chain_name] = VirtualNet(
|
|
61
|
+
chain_id=chain.chain_id,
|
|
62
|
+
funds_requirements={
|
|
63
|
+
"all": FundRequirements(
|
|
64
|
+
native_eth=settings.tenderly_native_funds,
|
|
65
|
+
tokens=tokens,
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return TenderlyConfig(vnets=vnets)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _delete_vnet(
|
|
74
|
+
tenderly_access_key: str, account_slug: str, project_slug: str, vnet_id: str
|
|
75
|
+
) -> None:
|
|
76
|
+
url = f"https://api.tenderly.co/api/v1/account/{account_slug}/project/{project_slug}/vnets/{vnet_id}"
|
|
77
|
+
requests.delete(
|
|
78
|
+
url=url,
|
|
79
|
+
timeout=300,
|
|
80
|
+
headers={"Accept": "application/json", "X-Access-Key": tenderly_access_key},
|
|
81
|
+
)
|
|
82
|
+
print(f"Deleted vnet {vnet_id}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _create_vnet(
|
|
86
|
+
tenderly_access_key: str,
|
|
87
|
+
account_slug: str,
|
|
88
|
+
project_slug: str,
|
|
89
|
+
network_id: int,
|
|
90
|
+
chain_id: int,
|
|
91
|
+
vnet_slug: str,
|
|
92
|
+
vnet_display_name: str,
|
|
93
|
+
block_number: Optional[str] = "latest",
|
|
94
|
+
) -> Tuple[str | None, str | None, str | None]:
|
|
95
|
+
# Define the payload for the fork creation
|
|
96
|
+
payload = {
|
|
97
|
+
"slug": vnet_slug,
|
|
98
|
+
"display_name": vnet_display_name,
|
|
99
|
+
"fork_config": {"network_id": network_id, "block_number": str(block_number)},
|
|
100
|
+
"virtual_network_config": {"chain_config": {"chain_id": chain_id}},
|
|
101
|
+
"sync_state_config": {"enabled": False},
|
|
102
|
+
"explorer_page_config": {
|
|
103
|
+
"enabled": False,
|
|
104
|
+
"verification_visibility": "bytecode",
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
url = f"https://api.tenderly.co/api/v1/account/{account_slug}/project/{project_slug}/vnets"
|
|
109
|
+
response = requests.post(
|
|
110
|
+
url=url,
|
|
111
|
+
timeout=300,
|
|
112
|
+
headers={
|
|
113
|
+
"Accept": "application/json",
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
"X-Access-Key": tenderly_access_key,
|
|
116
|
+
},
|
|
117
|
+
data=json.dumps(payload),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
json_response = response.json()
|
|
121
|
+
vnet_id = json_response.get("id")
|
|
122
|
+
admin_rpc = next(
|
|
123
|
+
(rpc["url"] for rpc in json_response.get("rpcs", []) if rpc["name"] == "Admin RPC"),
|
|
124
|
+
None,
|
|
125
|
+
)
|
|
126
|
+
public_rpc = next(
|
|
127
|
+
(rpc["url"] for rpc in json_response.get("rpcs", []) if rpc["name"] == "Public RPC"),
|
|
128
|
+
None,
|
|
129
|
+
)
|
|
130
|
+
print(f"Created vnet of chain_id={network_id} at block number {block_number}")
|
|
131
|
+
return vnet_id, admin_rpc, public_rpc
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _generate_vnet_slug(preffix: str = "vnet", length: int = 4):
|
|
135
|
+
characters = string.ascii_lowercase
|
|
136
|
+
return (
|
|
137
|
+
preffix + "-" + "".join(random.choice(characters) for _ in range(length)) # nosec
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def update_rpc_variables(tenderly_config: TenderlyConfig) -> None:
|
|
142
|
+
"""Updates several files"""
|
|
143
|
+
with open(SECRETS_PATH, "r", encoding="utf-8") as file:
|
|
144
|
+
content = file.read()
|
|
145
|
+
|
|
146
|
+
for chain_name, vnet in tenderly_config.vnets.items():
|
|
147
|
+
pattern = rf"{chain_name.lower()}_test_rpc=(\S+)"
|
|
148
|
+
|
|
149
|
+
if re.search(pattern, content, re.MULTILINE):
|
|
150
|
+
content = re.sub(
|
|
151
|
+
pattern,
|
|
152
|
+
f"{chain_name.lower()}_test_rpc={vnet.public_rpc}",
|
|
153
|
+
content,
|
|
154
|
+
flags=re.MULTILINE,
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
if content and not content.endswith("\n"):
|
|
158
|
+
content += "\n"
|
|
159
|
+
content += f"{chain_name.lower()}_test_rpc={vnet.public_rpc}\n"
|
|
160
|
+
|
|
161
|
+
with open(SECRETS_PATH, "w", encoding="utf-8") as file:
|
|
162
|
+
file.write(content)
|
|
163
|
+
|
|
164
|
+
print("Updated RPCs in secrets.env")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _fund_wallet( # nosec
|
|
168
|
+
admin_rpc: str,
|
|
169
|
+
wallet_addresses: List[str],
|
|
170
|
+
amount_eth: float,
|
|
171
|
+
native_or_token_address: str = "native",
|
|
172
|
+
) -> None:
|
|
173
|
+
if native_or_token_address == "native": # nosec
|
|
174
|
+
json_data = {
|
|
175
|
+
"jsonrpc": "2.0",
|
|
176
|
+
"method": "tenderly_setBalance",
|
|
177
|
+
"params": [
|
|
178
|
+
wallet_addresses,
|
|
179
|
+
hex(Web3.to_wei(amount_eth, "ether")), # to wei
|
|
180
|
+
],
|
|
181
|
+
"id": "1234",
|
|
182
|
+
}
|
|
183
|
+
else:
|
|
184
|
+
json_data = {
|
|
185
|
+
"jsonrpc": "2.0",
|
|
186
|
+
"method": "tenderly_setErc20Balance",
|
|
187
|
+
"params": [
|
|
188
|
+
native_or_token_address,
|
|
189
|
+
wallet_addresses,
|
|
190
|
+
hex(Web3.to_wei(amount_eth, "ether")), # to wei
|
|
191
|
+
],
|
|
192
|
+
"id": "1234",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
response = requests.post(
|
|
196
|
+
url=admin_rpc,
|
|
197
|
+
timeout=300,
|
|
198
|
+
headers={"Content-Type": "application/json"},
|
|
199
|
+
json=json_data,
|
|
200
|
+
)
|
|
201
|
+
if response.status_code != 200:
|
|
202
|
+
print(response.status_code)
|
|
203
|
+
try:
|
|
204
|
+
print(response.json())
|
|
205
|
+
except requests.exceptions.JSONDecodeError: # type: ignore
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _process_vnet(
|
|
210
|
+
vnet_name: str,
|
|
211
|
+
vnet,
|
|
212
|
+
tenderly_access_key: str,
|
|
213
|
+
account_slug: str,
|
|
214
|
+
project_slug: str,
|
|
215
|
+
tenderly_config,
|
|
216
|
+
) -> bool:
|
|
217
|
+
"""Process a single vnet: delete old, create new, capture block."""
|
|
218
|
+
# Delete existing vnet
|
|
219
|
+
if vnet.vnet_id:
|
|
220
|
+
_delete_vnet(
|
|
221
|
+
tenderly_access_key=tenderly_access_key,
|
|
222
|
+
account_slug=account_slug,
|
|
223
|
+
project_slug=project_slug,
|
|
224
|
+
vnet_id=vnet.vnet_id,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Create new network
|
|
228
|
+
vnet_slug = _generate_vnet_slug(preffix=vnet_name.lower())
|
|
229
|
+
vnet_id, admin_rpc, public_rpc = _create_vnet(
|
|
230
|
+
tenderly_access_key=tenderly_access_key,
|
|
231
|
+
account_slug=account_slug,
|
|
232
|
+
project_slug=project_slug,
|
|
233
|
+
network_id=vnet.chain_id,
|
|
234
|
+
chain_id=vnet.chain_id,
|
|
235
|
+
vnet_slug=vnet_slug,
|
|
236
|
+
vnet_display_name=vnet_slug,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if not vnet_id or not admin_rpc or not public_rpc:
|
|
240
|
+
print(f"Failed to create valid vnet for {vnet_name}")
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
vnet.vnet_id = vnet_id
|
|
244
|
+
vnet.admin_rpc = admin_rpc
|
|
245
|
+
vnet.public_rpc = public_rpc
|
|
246
|
+
vnet.vnet_slug = vnet_slug
|
|
247
|
+
|
|
248
|
+
# Capture initial block
|
|
249
|
+
try:
|
|
250
|
+
w3 = Web3(Web3.HTTPProvider(public_rpc))
|
|
251
|
+
start_block = w3.eth.block_number
|
|
252
|
+
vnet.initial_block = start_block
|
|
253
|
+
print(f"Captured initial block for {vnet_name}: {start_block}")
|
|
254
|
+
except Exception as e:
|
|
255
|
+
print(f"Failed to capture initial block: {e}")
|
|
256
|
+
vnet.initial_block = 0
|
|
257
|
+
|
|
258
|
+
tenderly_config.save()
|
|
259
|
+
update_rpc_variables(tenderly_config)
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _fund_vnet_accounts(vnet, keys) -> None:
|
|
264
|
+
"""Fund all accounts for a vnet based on requirements."""
|
|
265
|
+
for account_tags, requirement in vnet.funds_requirements.items():
|
|
266
|
+
tags = account_tags.split(",")
|
|
267
|
+
if account_tags != "all":
|
|
268
|
+
addresses = []
|
|
269
|
+
for tag in tags:
|
|
270
|
+
if acc := keys.get_account(tag):
|
|
271
|
+
addresses.append(acc.address)
|
|
272
|
+
else:
|
|
273
|
+
addresses = list(keys.accounts.keys())
|
|
274
|
+
|
|
275
|
+
if not addresses:
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
if requirement.native_eth > 0:
|
|
279
|
+
_fund_wallet(
|
|
280
|
+
admin_rpc=vnet.admin_rpc,
|
|
281
|
+
wallet_addresses=addresses,
|
|
282
|
+
amount_eth=requirement.native_eth,
|
|
283
|
+
native_or_token_address="native",
|
|
284
|
+
)
|
|
285
|
+
print(f"Funded {tags} with {requirement.native_eth} native")
|
|
286
|
+
|
|
287
|
+
for token in requirement.tokens:
|
|
288
|
+
_fund_wallet(
|
|
289
|
+
admin_rpc=vnet.admin_rpc,
|
|
290
|
+
wallet_addresses=addresses,
|
|
291
|
+
amount_eth=token.amount_eth,
|
|
292
|
+
native_or_token_address=str(token.address),
|
|
293
|
+
)
|
|
294
|
+
print(f"Funded {tags} with {token.amount_eth} {token.symbol}")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main() -> None:
|
|
298
|
+
"""Main - uses tenderly_profile from settings."""
|
|
299
|
+
profile = settings.tenderly_profile
|
|
300
|
+
print(f"Recreating Tenderly Networks (Profile {profile})")
|
|
301
|
+
|
|
302
|
+
account_slug, project_slug, tenderly_access_key = get_tenderly_credentials()
|
|
303
|
+
|
|
304
|
+
if not account_slug or not project_slug or not tenderly_access_key:
|
|
305
|
+
print(f"Missing Tenderly environment variables for profile {profile}")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
config_path = get_tenderly_config_path(profile)
|
|
309
|
+
|
|
310
|
+
if not config_path.exists():
|
|
311
|
+
print(f"Generating new config file: {config_path}")
|
|
312
|
+
tenderly_config = _generate_default_config()
|
|
313
|
+
tenderly_config.save(config_path)
|
|
314
|
+
|
|
315
|
+
tenderly_config = TenderlyConfig.load(config_path)
|
|
316
|
+
|
|
317
|
+
for vnet_name, vnet in tenderly_config.vnets.items():
|
|
318
|
+
if not _process_vnet(
|
|
319
|
+
vnet_name, vnet, tenderly_access_key, account_slug, project_slug, tenderly_config
|
|
320
|
+
):
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
# Fund wallets
|
|
324
|
+
keys = KeyStorage()
|
|
325
|
+
from iwa.core.services import AccountService, SafeService
|
|
326
|
+
|
|
327
|
+
account_service = AccountService(keys)
|
|
328
|
+
safe_service = SafeService(keys, account_service)
|
|
329
|
+
|
|
330
|
+
_fund_vnet_accounts(vnet, keys)
|
|
331
|
+
|
|
332
|
+
# Redeploy safes for Gnosis
|
|
333
|
+
if vnet_name == "Gnosis":
|
|
334
|
+
safe_service.redeploy_safes()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == "__main__": # pragma: no cover
|
|
338
|
+
parser = argparse.ArgumentParser(description="Reset Tenderly networks")
|
|
339
|
+
parser.add_argument(
|
|
340
|
+
"--profile",
|
|
341
|
+
"-p",
|
|
342
|
+
type=int,
|
|
343
|
+
default=1,
|
|
344
|
+
choices=[1, 2, 3],
|
|
345
|
+
help="Tenderly profile to use (1 or 2)",
|
|
346
|
+
)
|
|
347
|
+
args = parser.parse_args()
|
|
348
|
+
|
|
349
|
+
# Set profile env var
|
|
350
|
+
os.environ["TENDERLY_PROFILE"] = str(args.profile)
|
|
351
|
+
|
|
352
|
+
# Reset the singleton to reload with new env
|
|
353
|
+
from iwa.core.settings import Settings
|
|
354
|
+
|
|
355
|
+
Settings._instance = None # type: ignore
|
|
356
|
+
|
|
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
|
|
361
|
+
|
|
362
|
+
main()
|