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,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate and restore wallet backup."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
|
|
10
|
+
DATA_DIR = PROJECT_ROOT / "data"
|
|
11
|
+
WALLET_PATH = DATA_DIR / "wallet.json"
|
|
12
|
+
BACKUP_DIR = DATA_DIR / "backup"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate_wallet_backup(backup_path: Path) -> tuple[bool, str, int]:
|
|
16
|
+
"""Validate that backup file is a valid wallet using Pydantic models.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Tuple of (is_valid, message, account_count)
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
from iwa.core.keys import EncryptedAccount, StoredSafeAccount
|
|
23
|
+
from iwa.core.models import EthereumAddress
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with open(backup_path, "r", encoding="utf-8") as f:
|
|
27
|
+
data = json.load(f)
|
|
28
|
+
except json.JSONDecodeError as e:
|
|
29
|
+
return False, f"Invalid JSON: {e}", 0
|
|
30
|
+
|
|
31
|
+
if not isinstance(data, dict) or "accounts" not in data:
|
|
32
|
+
return False, "Missing 'accounts' key", 0
|
|
33
|
+
|
|
34
|
+
accounts = data.get("accounts", {})
|
|
35
|
+
if not isinstance(accounts, dict):
|
|
36
|
+
return False, "'accounts' must be a dictionary", 0
|
|
37
|
+
|
|
38
|
+
# Validate each account using Pydantic models
|
|
39
|
+
for address, account_data in accounts.items():
|
|
40
|
+
try:
|
|
41
|
+
# Validate address
|
|
42
|
+
EthereumAddress(address)
|
|
43
|
+
|
|
44
|
+
# Validate account structure
|
|
45
|
+
if "signers" in account_data:
|
|
46
|
+
StoredSafeAccount(**account_data)
|
|
47
|
+
else:
|
|
48
|
+
EncryptedAccount(**account_data)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
return False, f"Invalid account {address[:10]}...: {e}", 0
|
|
51
|
+
|
|
52
|
+
return True, "Valid wallet structure", len(accounts)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def restore_backup(backup_name: str) -> int:
|
|
56
|
+
"""Restore wallet from backup with validation."""
|
|
57
|
+
backup_path = BACKUP_DIR / backup_name
|
|
58
|
+
|
|
59
|
+
if not backup_path.exists():
|
|
60
|
+
print(f"Error: Backup file not found: {backup_path}")
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
is_valid, message, num_accounts = validate_wallet_backup(backup_path)
|
|
64
|
+
if not is_valid:
|
|
65
|
+
print(f"Error: {message}")
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
print(f"Backup validated: {num_accounts} account(s) found")
|
|
69
|
+
|
|
70
|
+
# Restore
|
|
71
|
+
shutil.copy2(backup_path, WALLET_PATH)
|
|
72
|
+
print(f"Restored wallet from {backup_name}")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
if len(sys.argv) != 2:
|
|
78
|
+
print("Usage: restore_backup.py <backup_filename>")
|
|
79
|
+
print("Example: restore_backup.py wallet.json.20260102_101400.bkp")
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
sys.exit(restore_backup(sys.argv[1]))
|
iwa/tui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TUI package for IWA."""
|
iwa/tui/app.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Main TUI Application module."""
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.widgets import Footer, Header, TabbedContent, TabPane
|
|
7
|
+
|
|
8
|
+
from iwa.core.wallet import Wallet
|
|
9
|
+
from iwa.tui.rpc import RPCView
|
|
10
|
+
from iwa.tui.screens.wallets import WalletsScreen
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IwaApp(App):
|
|
14
|
+
"""Iwa TUI Application."""
|
|
15
|
+
|
|
16
|
+
# ... (keep constants) ...
|
|
17
|
+
TITLE = "Iwa"
|
|
18
|
+
|
|
19
|
+
CSS = """
|
|
20
|
+
.header {
|
|
21
|
+
margin: 1 0;
|
|
22
|
+
text-style: bold;
|
|
23
|
+
}
|
|
24
|
+
.form-row {
|
|
25
|
+
height: 5;
|
|
26
|
+
margin: 1 0;
|
|
27
|
+
align: left middle;
|
|
28
|
+
}
|
|
29
|
+
.label {
|
|
30
|
+
margin: 1 1;
|
|
31
|
+
width: auto;
|
|
32
|
+
}
|
|
33
|
+
Input {
|
|
34
|
+
width: 15;
|
|
35
|
+
height: 3;
|
|
36
|
+
margin-right: 1;
|
|
37
|
+
}
|
|
38
|
+
Select {
|
|
39
|
+
width: 30;
|
|
40
|
+
height: 3;
|
|
41
|
+
margin-right: 1;
|
|
42
|
+
}
|
|
43
|
+
Button {
|
|
44
|
+
margin-right: 1;
|
|
45
|
+
}
|
|
46
|
+
#chain_row {
|
|
47
|
+
height: 3;
|
|
48
|
+
margin: 1 0;
|
|
49
|
+
align: left middle;
|
|
50
|
+
}
|
|
51
|
+
#chain_row Label {
|
|
52
|
+
margin: 1 1;
|
|
53
|
+
height: 1;
|
|
54
|
+
}
|
|
55
|
+
#tokens_row {
|
|
56
|
+
height: 3;
|
|
57
|
+
margin: 0 0;
|
|
58
|
+
}
|
|
59
|
+
#tokens_row Label {
|
|
60
|
+
margin: 1 1;
|
|
61
|
+
height: 1;
|
|
62
|
+
}
|
|
63
|
+
#token_toggles {
|
|
64
|
+
height: 3;
|
|
65
|
+
margin: 0 0;
|
|
66
|
+
}
|
|
67
|
+
#accounts_table {
|
|
68
|
+
margin-top: 1;
|
|
69
|
+
height: 1fr;
|
|
70
|
+
min-height: 10;
|
|
71
|
+
}
|
|
72
|
+
#tx_table {
|
|
73
|
+
height: 10;
|
|
74
|
+
}
|
|
75
|
+
Tab {
|
|
76
|
+
width: 20;
|
|
77
|
+
}
|
|
78
|
+
.btn-group {
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: auto;
|
|
81
|
+
align: center middle;
|
|
82
|
+
margin-top: 1;
|
|
83
|
+
margin-bottom: 1;
|
|
84
|
+
}
|
|
85
|
+
.create-btn {
|
|
86
|
+
margin-left: 1;
|
|
87
|
+
margin-right: 1;
|
|
88
|
+
width: auto;
|
|
89
|
+
min-width: 20;
|
|
90
|
+
}
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
BINDINGS = [
|
|
94
|
+
Binding("q", "quit", "Quit"),
|
|
95
|
+
Binding("r", "refresh", "Refresh"),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
def __init__(self):
|
|
99
|
+
"""Initialize the App."""
|
|
100
|
+
super().__init__()
|
|
101
|
+
# Configure logger for TUI
|
|
102
|
+
logger.remove()
|
|
103
|
+
logger.add("iwa.log", rotation="10 MB", level="INFO")
|
|
104
|
+
|
|
105
|
+
self.wallet = Wallet()
|
|
106
|
+
|
|
107
|
+
# Use PluginService from wallet
|
|
108
|
+
self.plugins = self.wallet.plugin_service.get_all_plugins()
|
|
109
|
+
|
|
110
|
+
def compose(self) -> ComposeResult:
|
|
111
|
+
"""Compose the application layout.
|
|
112
|
+
|
|
113
|
+
Sets up the Header, TabbedContent with Wallets, Plugins, and RPC views,
|
|
114
|
+
and the Footer.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
ComposeResult: The widgets to be shown in the app.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
yield Header(show_clock=True)
|
|
121
|
+
|
|
122
|
+
with TabbedContent(initial="wallets-tab"):
|
|
123
|
+
# Wallets first (default)
|
|
124
|
+
with TabPane("Wallets", id="wallets-tab"):
|
|
125
|
+
yield WalletsScreen(self.wallet)
|
|
126
|
+
|
|
127
|
+
# Plugin tabs (Olas)
|
|
128
|
+
for _name, plugin in self.plugins.items():
|
|
129
|
+
view = plugin.get_tui_view(wallet=self.wallet)
|
|
130
|
+
if view:
|
|
131
|
+
with TabPane(plugin.name.capitalize(), id=f"{plugin.name.lower()}-tab"):
|
|
132
|
+
yield view
|
|
133
|
+
|
|
134
|
+
# RPC Status last
|
|
135
|
+
with TabPane("RPC Status", id="rpc-tab"):
|
|
136
|
+
yield RPCView()
|
|
137
|
+
|
|
138
|
+
yield Footer()
|
|
139
|
+
|
|
140
|
+
def action_refresh(self) -> None:
|
|
141
|
+
"""Action handler for the 'refresh' key binding.
|
|
142
|
+
|
|
143
|
+
Triggers a refresh of the currently active view. Currently specifically
|
|
144
|
+
targets the WalletsScreen to reload accounts and balances.
|
|
145
|
+
"""
|
|
146
|
+
# Ideally, propagate refresh to active tab
|
|
147
|
+
# For now, just refresh wallets view explicitly if it's there
|
|
148
|
+
try:
|
|
149
|
+
wallets_screen = self.query_one(WalletsScreen)
|
|
150
|
+
wallets_screen.refresh_accounts()
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
def copy_to_clipboard(self, text: str) -> None:
|
|
155
|
+
"""Copy the provided text to the system clipboard.
|
|
156
|
+
|
|
157
|
+
Uses `pyperclip` to handle cross-platform clipboard operations.
|
|
158
|
+
Logs an error if the copy operation fails.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
text: The string to copy.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
import pyperclip
|
|
166
|
+
|
|
167
|
+
pyperclip.copy(str(text))
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"Failed to copy to clipboard: {e}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
app = IwaApp()
|
|
174
|
+
app.run()
|
iwa/tui/modals/base.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Modal screens for the IWA TUI."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Tuple
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Horizontal, Vertical
|
|
7
|
+
from textual.screen import ModalScreen
|
|
8
|
+
from textual.widgets import (
|
|
9
|
+
Button,
|
|
10
|
+
Input,
|
|
11
|
+
Label,
|
|
12
|
+
Select,
|
|
13
|
+
SelectionList,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from iwa.core.chain import ChainInterfaces
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CreateEOAModal(ModalScreen):
|
|
20
|
+
"""Modal screen for creating a new EOA wallet."""
|
|
21
|
+
|
|
22
|
+
CSS = """
|
|
23
|
+
CreateEOAModal {
|
|
24
|
+
align: center middle;
|
|
25
|
+
}
|
|
26
|
+
#dialog {
|
|
27
|
+
padding: 1 2;
|
|
28
|
+
width: 60;
|
|
29
|
+
height: auto;
|
|
30
|
+
border: thick $background 80%;
|
|
31
|
+
background: $surface;
|
|
32
|
+
}
|
|
33
|
+
#dialog Label {
|
|
34
|
+
width: 100%;
|
|
35
|
+
margin-bottom: 1;
|
|
36
|
+
}
|
|
37
|
+
.header {
|
|
38
|
+
text-align: center;
|
|
39
|
+
text-style: bold;
|
|
40
|
+
margin-bottom: 2;
|
|
41
|
+
}
|
|
42
|
+
#tag_input {
|
|
43
|
+
width: 100%;
|
|
44
|
+
margin-bottom: 2;
|
|
45
|
+
}
|
|
46
|
+
#btn_row {
|
|
47
|
+
height: 3;
|
|
48
|
+
width: 100%;
|
|
49
|
+
align: center middle;
|
|
50
|
+
}
|
|
51
|
+
Button {
|
|
52
|
+
margin: 0 1;
|
|
53
|
+
}
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def compose(self) -> ComposeResult:
|
|
57
|
+
"""Compose the modal UI."""
|
|
58
|
+
with Vertical(id="dialog"):
|
|
59
|
+
yield Label("Create New EOA Wallet", classes="header")
|
|
60
|
+
yield Label("Tag (Name):")
|
|
61
|
+
yield Input(placeholder="e.g. My EOA", id="tag_input")
|
|
62
|
+
with Horizontal(id="btn_row"):
|
|
63
|
+
yield Button("Cancel", id="cancel")
|
|
64
|
+
yield Button("Create", variant="primary", id="create")
|
|
65
|
+
|
|
66
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
67
|
+
"""Handle button press."""
|
|
68
|
+
if event.button.id == "create":
|
|
69
|
+
tag = self.query_one("#tag_input").value
|
|
70
|
+
self.dismiss(tag)
|
|
71
|
+
elif event.button.id == "cancel":
|
|
72
|
+
self.dismiss(None)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CreateSafeModal(ModalScreen):
|
|
76
|
+
"""Modal screen for creating a new Safe wallet."""
|
|
77
|
+
|
|
78
|
+
CSS = """
|
|
79
|
+
CreateSafeModal {
|
|
80
|
+
align: center middle;
|
|
81
|
+
}
|
|
82
|
+
#dialog {
|
|
83
|
+
padding: 1 2;
|
|
84
|
+
width: 70;
|
|
85
|
+
height: auto;
|
|
86
|
+
max-height: 90%;
|
|
87
|
+
border: thick $background 80%;
|
|
88
|
+
background: $surface;
|
|
89
|
+
overflow-y: auto;
|
|
90
|
+
}
|
|
91
|
+
#dialog Label {
|
|
92
|
+
width: 100%;
|
|
93
|
+
margin-bottom: 1;
|
|
94
|
+
}
|
|
95
|
+
.header {
|
|
96
|
+
text-align: center;
|
|
97
|
+
text-style: bold;
|
|
98
|
+
margin-bottom: 2;
|
|
99
|
+
}
|
|
100
|
+
#tag_input {
|
|
101
|
+
width: 100%;
|
|
102
|
+
margin-bottom: 2;
|
|
103
|
+
}
|
|
104
|
+
#threshold_input {
|
|
105
|
+
width: 100%;
|
|
106
|
+
margin-bottom: 2;
|
|
107
|
+
}
|
|
108
|
+
SelectionList {
|
|
109
|
+
height: 8;
|
|
110
|
+
margin-bottom: 2;
|
|
111
|
+
border: solid $secondary;
|
|
112
|
+
}
|
|
113
|
+
#btn_row {
|
|
114
|
+
height: 3;
|
|
115
|
+
width: 100%;
|
|
116
|
+
align: center middle;
|
|
117
|
+
}
|
|
118
|
+
Button {
|
|
119
|
+
margin: 0 1;
|
|
120
|
+
}
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, existing_accounts: List[Tuple[str, str]]):
|
|
124
|
+
"""Init with list of (tag, address) tuples."""
|
|
125
|
+
super().__init__()
|
|
126
|
+
self.existing_accounts = existing_accounts
|
|
127
|
+
|
|
128
|
+
def compose(self) -> ComposeResult:
|
|
129
|
+
"""Compose the modal UI."""
|
|
130
|
+
with Vertical(id="dialog"):
|
|
131
|
+
yield Label("Create New Safe Wallet", classes="header")
|
|
132
|
+
|
|
133
|
+
yield Label("Tag (Name):")
|
|
134
|
+
yield Input(placeholder="e.g. My Safe", id="tag_input")
|
|
135
|
+
|
|
136
|
+
yield Label("Threshold (Min signatures):")
|
|
137
|
+
yield Input(placeholder="1", id="threshold_input", type="integer")
|
|
138
|
+
|
|
139
|
+
yield Label("Owners (select multiple):")
|
|
140
|
+
options = [(f"{tag} ({addr})", addr) for tag, addr in self.existing_accounts]
|
|
141
|
+
yield SelectionList[str](*options, id="owners_list")
|
|
142
|
+
|
|
143
|
+
yield Label("Chains (select multiple):")
|
|
144
|
+
chain_options = [(name.title(), name) for name, _ in ChainInterfaces().items()]
|
|
145
|
+
yield SelectionList[str](*chain_options, id="chains_list")
|
|
146
|
+
|
|
147
|
+
with Horizontal(id="btn_row"):
|
|
148
|
+
yield Button("Cancel", id="cancel")
|
|
149
|
+
yield Button("Create", variant="primary", id="create")
|
|
150
|
+
|
|
151
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
152
|
+
"""Handle button press."""
|
|
153
|
+
if event.button.id == "create":
|
|
154
|
+
tag = self.query_one("#tag_input", Input).value
|
|
155
|
+
threshold = int(self.query_one("#threshold_input", Input).value or "1")
|
|
156
|
+
owners = self.query_one("#owners_list", SelectionList).selected
|
|
157
|
+
chains = self.query_one("#chains_list", SelectionList).selected
|
|
158
|
+
self.dismiss({"tag": tag, "threshold": threshold, "owners": owners, "chains": chains})
|
|
159
|
+
elif event.button.id == "cancel":
|
|
160
|
+
self.dismiss(None)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class StakeServiceModal(ModalScreen):
|
|
164
|
+
"""Modal screen for selecting a staking contract."""
|
|
165
|
+
|
|
166
|
+
CSS = """
|
|
167
|
+
StakeServiceModal {
|
|
168
|
+
align: center middle;
|
|
169
|
+
}
|
|
170
|
+
#dialog {
|
|
171
|
+
padding: 1 2;
|
|
172
|
+
width: 60;
|
|
173
|
+
height: auto;
|
|
174
|
+
border: thick $background 80%;
|
|
175
|
+
background: $surface;
|
|
176
|
+
}
|
|
177
|
+
#dialog Label {
|
|
178
|
+
width: 100%;
|
|
179
|
+
margin-bottom: 1;
|
|
180
|
+
}
|
|
181
|
+
.header {
|
|
182
|
+
text-align: center;
|
|
183
|
+
text-style: bold;
|
|
184
|
+
margin-bottom: 2;
|
|
185
|
+
}
|
|
186
|
+
Select {
|
|
187
|
+
width: 100%;
|
|
188
|
+
margin-bottom: 2;
|
|
189
|
+
}
|
|
190
|
+
#btn_row {
|
|
191
|
+
height: 3;
|
|
192
|
+
width: 100%;
|
|
193
|
+
align: center middle;
|
|
194
|
+
}
|
|
195
|
+
Button {
|
|
196
|
+
margin: 0 1;
|
|
197
|
+
}
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, contracts: List[Tuple[str, str]]):
|
|
201
|
+
"""Init with list of (name, address) tuples."""
|
|
202
|
+
super().__init__()
|
|
203
|
+
self.contracts = contracts
|
|
204
|
+
|
|
205
|
+
def compose(self) -> ComposeResult:
|
|
206
|
+
"""Compose the modal UI."""
|
|
207
|
+
with Vertical(id="dialog"):
|
|
208
|
+
yield Label("Stake Service", classes="header")
|
|
209
|
+
yield Label("Select Staking Contract:")
|
|
210
|
+
options = [(name, addr) for name, addr in self.contracts]
|
|
211
|
+
yield Select(options, prompt="Select a contract...", id="contract_select")
|
|
212
|
+
with Horizontal(id="btn_row"):
|
|
213
|
+
yield Button("Cancel", id="cancel")
|
|
214
|
+
yield Button("Stake", variant="primary", id="stake")
|
|
215
|
+
|
|
216
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
217
|
+
"""Handle button press."""
|
|
218
|
+
if event.button.id == "stake":
|
|
219
|
+
contract_address = self.query_one("#contract_select", Select).value
|
|
220
|
+
if contract_address == Select.BLANK:
|
|
221
|
+
return
|
|
222
|
+
self.dismiss(contract_address)
|
|
223
|
+
elif event.button.id == "cancel":
|
|
224
|
+
self.dismiss(None)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class CreateServiceModal(ModalScreen):
|
|
228
|
+
"""Modal screen for creating a new Olas service."""
|
|
229
|
+
|
|
230
|
+
CSS = """
|
|
231
|
+
CreateServiceModal {
|
|
232
|
+
align: center middle;
|
|
233
|
+
}
|
|
234
|
+
#dialog {
|
|
235
|
+
padding: 1 2;
|
|
236
|
+
width: 65;
|
|
237
|
+
height: auto;
|
|
238
|
+
border: thick $background 80%;
|
|
239
|
+
background: $surface;
|
|
240
|
+
}
|
|
241
|
+
#dialog Label {
|
|
242
|
+
width: 100%;
|
|
243
|
+
margin-bottom: 1;
|
|
244
|
+
}
|
|
245
|
+
.header {
|
|
246
|
+
text-align: center;
|
|
247
|
+
text-style: bold;
|
|
248
|
+
margin-bottom: 2;
|
|
249
|
+
}
|
|
250
|
+
Input {
|
|
251
|
+
width: 100%;
|
|
252
|
+
margin-bottom: 2;
|
|
253
|
+
}
|
|
254
|
+
Select {
|
|
255
|
+
width: 100%;
|
|
256
|
+
margin-bottom: 2;
|
|
257
|
+
}
|
|
258
|
+
#btn_row {
|
|
259
|
+
height: 3;
|
|
260
|
+
width: 100%;
|
|
261
|
+
align: center middle;
|
|
262
|
+
}
|
|
263
|
+
Button {
|
|
264
|
+
margin: 0 1;
|
|
265
|
+
}
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def __init__(
|
|
269
|
+
self,
|
|
270
|
+
chains: List[str],
|
|
271
|
+
default_chain: str = "gnosis",
|
|
272
|
+
staking_contracts: List[Tuple[str, str]] = None,
|
|
273
|
+
):
|
|
274
|
+
"""Init with list of available chains and staking contracts."""
|
|
275
|
+
super().__init__()
|
|
276
|
+
self.chains = chains
|
|
277
|
+
self.default_chain = default_chain
|
|
278
|
+
self.staking_contracts = staking_contracts or []
|
|
279
|
+
|
|
280
|
+
def compose(self) -> ComposeResult:
|
|
281
|
+
"""Compose the modal UI."""
|
|
282
|
+
with Vertical(id="dialog"):
|
|
283
|
+
yield Label("Create New Olas Service", classes="header")
|
|
284
|
+
|
|
285
|
+
yield Label("Service Name:")
|
|
286
|
+
yield Input(placeholder="e.g. My Trader", id="name_input")
|
|
287
|
+
|
|
288
|
+
yield Label("Chain:")
|
|
289
|
+
chain_options = [(c.title(), c) for c in self.chains]
|
|
290
|
+
yield Select(chain_options, value=self.default_chain, id="chain_select")
|
|
291
|
+
|
|
292
|
+
yield Label("Agent Type:")
|
|
293
|
+
agent_options = [("Trader", "trader")]
|
|
294
|
+
yield Select(agent_options, value="trader", id="agent_type_select")
|
|
295
|
+
|
|
296
|
+
yield Label("Staking Contract:")
|
|
297
|
+
contract_options = [("None (don't stake)", "")]
|
|
298
|
+
contract_options.extend([(name, addr) for name, addr in self.staking_contracts])
|
|
299
|
+
yield Select(contract_options, value="", id="staking_select")
|
|
300
|
+
|
|
301
|
+
with Horizontal(id="btn_row"):
|
|
302
|
+
yield Button("Cancel", id="cancel")
|
|
303
|
+
yield Button("Create", variant="primary", id="create")
|
|
304
|
+
|
|
305
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
306
|
+
"""Handle button press."""
|
|
307
|
+
if event.button.id == "create":
|
|
308
|
+
name = self.query_one("#name_input", Input).value
|
|
309
|
+
chain = self.query_one("#chain_select", Select).value
|
|
310
|
+
agent_type = self.query_one("#agent_type_select", Select).value
|
|
311
|
+
staking_contract = self.query_one("#staking_select", Select).value
|
|
312
|
+
if not name or chain == Select.BLANK:
|
|
313
|
+
return
|
|
314
|
+
self.dismiss(
|
|
315
|
+
{
|
|
316
|
+
"name": name,
|
|
317
|
+
"chain": chain,
|
|
318
|
+
"agent_type": agent_type if agent_type != Select.BLANK else "trader",
|
|
319
|
+
"staking_contract": staking_contract
|
|
320
|
+
if staking_contract != Select.BLANK
|
|
321
|
+
else None,
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
elif event.button.id == "cancel":
|
|
325
|
+
self.dismiss(None)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class FundServiceModal(ModalScreen):
|
|
329
|
+
"""Modal screen for funding Olas service accounts."""
|
|
330
|
+
|
|
331
|
+
CSS = """
|
|
332
|
+
FundServiceModal {
|
|
333
|
+
align: center middle;
|
|
334
|
+
}
|
|
335
|
+
#dialog {
|
|
336
|
+
padding: 1 2;
|
|
337
|
+
width: 60;
|
|
338
|
+
height: auto;
|
|
339
|
+
border: thick $background 80%;
|
|
340
|
+
background: $surface;
|
|
341
|
+
}
|
|
342
|
+
#dialog Label {
|
|
343
|
+
width: 100%;
|
|
344
|
+
margin-bottom: 1;
|
|
345
|
+
}
|
|
346
|
+
.header {
|
|
347
|
+
text-align: center;
|
|
348
|
+
text-style: bold;
|
|
349
|
+
margin-bottom: 2;
|
|
350
|
+
}
|
|
351
|
+
.desc {
|
|
352
|
+
color: $text-muted;
|
|
353
|
+
margin-bottom: 2;
|
|
354
|
+
}
|
|
355
|
+
Input {
|
|
356
|
+
width: 100%;
|
|
357
|
+
margin-bottom: 2;
|
|
358
|
+
}
|
|
359
|
+
#btn_row {
|
|
360
|
+
height: 3;
|
|
361
|
+
width: 100%;
|
|
362
|
+
align: center middle;
|
|
363
|
+
}
|
|
364
|
+
Button {
|
|
365
|
+
margin: 0 1;
|
|
366
|
+
}
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
def __init__(self, service_key: str, native_symbol: str = "xDAI"):
|
|
370
|
+
"""Init with service key and native currency symbol."""
|
|
371
|
+
super().__init__()
|
|
372
|
+
self.service_key = service_key
|
|
373
|
+
self.native_symbol = native_symbol
|
|
374
|
+
|
|
375
|
+
def compose(self) -> ComposeResult:
|
|
376
|
+
"""Compose the modal UI."""
|
|
377
|
+
with Vertical(id="dialog"):
|
|
378
|
+
yield Label("Fund Service", classes="header")
|
|
379
|
+
yield Label(f"Send {self.native_symbol} from master wallet:", classes="desc")
|
|
380
|
+
yield Label(f"Agent Amount ({self.native_symbol}):")
|
|
381
|
+
yield Input(placeholder="0.0", id="agent_amount", type="number")
|
|
382
|
+
yield Label(f"Safe Amount ({self.native_symbol}):")
|
|
383
|
+
yield Input(placeholder="0.0", id="safe_amount", type="number")
|
|
384
|
+
with Horizontal(id="btn_row"):
|
|
385
|
+
yield Button("Cancel", id="cancel")
|
|
386
|
+
yield Button("Fund", variant="primary", id="fund")
|
|
387
|
+
|
|
388
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
389
|
+
"""Handle button press."""
|
|
390
|
+
if event.button.id == "fund":
|
|
391
|
+
try:
|
|
392
|
+
agent_amount = float(self.query_one("#agent_amount", Input).value or "0")
|
|
393
|
+
safe_amount = float(self.query_one("#safe_amount", Input).value or "0")
|
|
394
|
+
except ValueError:
|
|
395
|
+
return
|
|
396
|
+
if agent_amount <= 0 and safe_amount <= 0:
|
|
397
|
+
return
|
|
398
|
+
self.dismiss(
|
|
399
|
+
{
|
|
400
|
+
"service_key": self.service_key,
|
|
401
|
+
"agent_amount": agent_amount,
|
|
402
|
+
"safe_amount": safe_amount,
|
|
403
|
+
}
|
|
404
|
+
)
|
|
405
|
+
elif event.button.id == "cancel":
|
|
406
|
+
self.dismiss(None)
|