wayfinder-paths 0.1.1__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.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/CONFIG_GUIDE.md +394 -0
- wayfinder_paths/__init__.py +21 -0
- wayfinder_paths/config.example.json +20 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +13 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +90 -0
- wayfinder_paths/core/clients/ClientManager.py +231 -0
- wayfinder_paths/core/clients/HyperlendClient.py +151 -0
- wayfinder_paths/core/clients/LedgerClient.py +222 -0
- wayfinder_paths/core/clients/PoolClient.py +96 -0
- wayfinder_paths/core/clients/SimulationClient.py +180 -0
- wayfinder_paths/core/clients/TokenClient.py +73 -0
- wayfinder_paths/core/clients/TransactionClient.py +47 -0
- wayfinder_paths/core/clients/WalletClient.py +90 -0
- wayfinder_paths/core/clients/WayfinderClient.py +258 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +295 -0
- wayfinder_paths/core/clients/sdk_example.py +115 -0
- wayfinder_paths/core/config.py +369 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +25 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/VaultJob.py +182 -0
- wayfinder_paths/core/engine/__init__.py +5 -0
- wayfinder_paths/core/engine/manifest.py +97 -0
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +177 -0
- wayfinder_paths/core/services/local_evm_txn.py +429 -0
- wayfinder_paths/core/services/local_token_txn.py +231 -0
- wayfinder_paths/core/services/web3_service.py +45 -0
- wayfinder_paths/core/settings.py +61 -0
- wayfinder_paths/core/strategies/Strategy.py +183 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +165 -0
- wayfinder_paths/core/utils/wallets.py +77 -0
- wayfinder_paths/core/wallets/README.md +91 -0
- wayfinder_paths/core/wallets/WalletManager.py +56 -0
- wayfinder_paths/core/wallets/__init__.py +7 -0
- wayfinder_paths/run_strategy.py +409 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +160 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/tests/__init__.py +0 -0
- wayfinder_paths/tests/test_smoke_manifest.py +48 -0
- wayfinder_paths/tests/test_test_coverage.py +212 -0
- wayfinder_paths/tests/test_utils.py +64 -0
- wayfinder_paths/vaults/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
- wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
- wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
- wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
- wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
- wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
- wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
- wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
- wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
- wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
- wayfinder_paths/vaults/strategies/__init__.py +0 -0
- wayfinder_paths/vaults/strategies/config.py +85 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
- wayfinder_paths/vaults/templates/adapter/README.md +105 -0
- wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
- wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
- wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/vaults/templates/strategy/README.md +152 -0
- wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
- wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
- wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
- wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
- wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
- wayfinder_paths-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HyperLend ABI constants for smart contract interactions.
|
|
3
|
+
|
|
4
|
+
This module contains ABI definitions for HyperLend protocol contracts,
|
|
5
|
+
including Pool, Protocol Data Provider, Wrapped Token Gateway, and WETH contracts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Minimal Pool ABI for supply and deposit operations
|
|
9
|
+
POOL_ABI = [
|
|
10
|
+
{
|
|
11
|
+
"name": "supply",
|
|
12
|
+
"type": "function",
|
|
13
|
+
"stateMutability": "nonpayable",
|
|
14
|
+
"inputs": [
|
|
15
|
+
{"name": "asset", "type": "address"},
|
|
16
|
+
{"name": "amount", "type": "uint256"},
|
|
17
|
+
{"name": "onBehalfOf", "type": "address"},
|
|
18
|
+
{"name": "referralCode", "type": "uint16"},
|
|
19
|
+
],
|
|
20
|
+
"outputs": [],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "deposit",
|
|
24
|
+
"type": "function",
|
|
25
|
+
"stateMutability": "nonpayable",
|
|
26
|
+
"inputs": [
|
|
27
|
+
{"name": "asset", "type": "address"},
|
|
28
|
+
{"name": "amount", "type": "uint256"},
|
|
29
|
+
{"name": "onBehalfOf", "type": "address"},
|
|
30
|
+
{"name": "referralCode", "type": "uint16"},
|
|
31
|
+
],
|
|
32
|
+
"outputs": [],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "withdraw",
|
|
36
|
+
"type": "function",
|
|
37
|
+
"stateMutability": "nonpayable",
|
|
38
|
+
"inputs": [
|
|
39
|
+
{"name": "asset", "type": "address"},
|
|
40
|
+
{"name": "amount", "type": "uint256"},
|
|
41
|
+
{"name": "to", "type": "address"},
|
|
42
|
+
],
|
|
43
|
+
"outputs": [],
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Protocol Data Provider ABI for reserve token addresses and user data
|
|
48
|
+
PROTOCOL_DATA_PROVIDER_ABI = [
|
|
49
|
+
{
|
|
50
|
+
"type": "function",
|
|
51
|
+
"stateMutability": "view",
|
|
52
|
+
"name": "getReserveTokensAddresses",
|
|
53
|
+
"inputs": [{"name": "asset", "type": "address"}],
|
|
54
|
+
"outputs": [
|
|
55
|
+
{"name": "aTokenAddress", "type": "address"},
|
|
56
|
+
{"name": "stableDebtTokenAddress", "type": "address"},
|
|
57
|
+
{"name": "variableDebtTokenAddress", "type": "address"},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "function",
|
|
62
|
+
"stateMutability": "view",
|
|
63
|
+
"name": "getUserReserveData",
|
|
64
|
+
"inputs": [
|
|
65
|
+
{"name": "asset", "type": "address"},
|
|
66
|
+
{"name": "user", "type": "address"},
|
|
67
|
+
],
|
|
68
|
+
"outputs": [
|
|
69
|
+
{"name": "currentATokenBalance", "type": "uint256"},
|
|
70
|
+
{"name": "currentStableDebt", "type": "uint256"},
|
|
71
|
+
{"name": "currentVariableDebt", "type": "uint256"},
|
|
72
|
+
{"name": "liquidityRate", "type": "uint256"},
|
|
73
|
+
{"name": "stableBorrowRate", "type": "uint256"},
|
|
74
|
+
{"name": "variableBorrowRate", "type": "uint256"},
|
|
75
|
+
{"name": "liquidityIndex", "type": "uint256"},
|
|
76
|
+
{"name": "healthFactor", "type": "uint256"},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# Wrapped Token Gateway ABI for native token operations
|
|
82
|
+
WRAPPED_TOKEN_GATEWAY_ABI = [
|
|
83
|
+
{
|
|
84
|
+
"type": "function",
|
|
85
|
+
"stateMutability": "view",
|
|
86
|
+
"name": "getWETHAddress",
|
|
87
|
+
"inputs": [],
|
|
88
|
+
"outputs": [{"type": "address"}],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"type": "function",
|
|
92
|
+
"stateMutability": "payable",
|
|
93
|
+
"name": "depositETH",
|
|
94
|
+
"inputs": [
|
|
95
|
+
{"type": "address"},
|
|
96
|
+
{"type": "address"},
|
|
97
|
+
{"type": "uint16"},
|
|
98
|
+
],
|
|
99
|
+
"outputs": [],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"type": "function",
|
|
103
|
+
"stateMutability": "nonpayable",
|
|
104
|
+
"name": "withdrawETH",
|
|
105
|
+
"inputs": [
|
|
106
|
+
{"type": "address"},
|
|
107
|
+
{"type": "uint256"},
|
|
108
|
+
{"type": "address"},
|
|
109
|
+
],
|
|
110
|
+
"outputs": [],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"type": "function",
|
|
114
|
+
"stateMutability": "payable",
|
|
115
|
+
"name": "repayETH",
|
|
116
|
+
"inputs": [
|
|
117
|
+
{"type": "address"},
|
|
118
|
+
{"type": "uint256"},
|
|
119
|
+
{"type": "address"},
|
|
120
|
+
],
|
|
121
|
+
"outputs": [],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"type": "function",
|
|
125
|
+
"stateMutability": "nonpayable",
|
|
126
|
+
"name": "borrowETH",
|
|
127
|
+
"inputs": [
|
|
128
|
+
{"type": "address"},
|
|
129
|
+
{"type": "uint256"},
|
|
130
|
+
{"type": "uint16"},
|
|
131
|
+
],
|
|
132
|
+
"outputs": [],
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
# WETH ABI for native token wrapping
|
|
137
|
+
WETH_ABI = [
|
|
138
|
+
{
|
|
139
|
+
"inputs": [],
|
|
140
|
+
"name": "deposit",
|
|
141
|
+
"outputs": [],
|
|
142
|
+
"stateMutability": "payable",
|
|
143
|
+
"type": "function",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"inputs": [{"name": "wad", "type": "uint256"}],
|
|
147
|
+
"name": "withdraw",
|
|
148
|
+
"outputs": [],
|
|
149
|
+
"stateMutability": "nonpayable",
|
|
150
|
+
"type": "function",
|
|
151
|
+
},
|
|
152
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from wayfinder_paths.core.clients.ClientManager import ClientManager
|
|
8
|
+
from wayfinder_paths.core.config import VaultConfig
|
|
9
|
+
from wayfinder_paths.core.strategies.Strategy import Strategy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VaultJob:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
strategy: Strategy,
|
|
16
|
+
config: VaultConfig,
|
|
17
|
+
clients: dict[str, Any] | None = None,
|
|
18
|
+
skip_auth: bool = False,
|
|
19
|
+
api_key: str | None = None,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Initialize a VaultJob.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
strategy: The strategy to execute.
|
|
26
|
+
config: Vault configuration.
|
|
27
|
+
clients: Optional dict of pre-instantiated clients to inject directly.
|
|
28
|
+
skip_auth: If True, skips authentication (for SDK usage).
|
|
29
|
+
api_key: Optional API key for service account authentication.
|
|
30
|
+
If provided, will be passed to ClientManager and strategy.
|
|
31
|
+
"""
|
|
32
|
+
self.strategy = strategy
|
|
33
|
+
self.config = config
|
|
34
|
+
|
|
35
|
+
self.job_id = strategy.name or "unknown"
|
|
36
|
+
self.clients = ClientManager(
|
|
37
|
+
clients=clients, skip_auth=skip_auth, api_key=api_key
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def _setup_strategy(self):
|
|
41
|
+
"""Setup the strategy instance"""
|
|
42
|
+
if not self.strategy:
|
|
43
|
+
raise ValueError("No strategy provided to VaultJob")
|
|
44
|
+
|
|
45
|
+
self.strategy.log = self.log
|
|
46
|
+
|
|
47
|
+
def _is_using_api_key(self) -> bool:
|
|
48
|
+
"""Check if API key authentication is being used."""
|
|
49
|
+
if self.clients._api_key:
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
if self.clients.auth:
|
|
53
|
+
try:
|
|
54
|
+
creds = self.clients.auth._load_config_credentials()
|
|
55
|
+
if creds.get("api_key"):
|
|
56
|
+
return True
|
|
57
|
+
if os.getenv("WAYFINDER_API_KEY"):
|
|
58
|
+
return True
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
async def setup(self):
|
|
65
|
+
"""
|
|
66
|
+
Initialize the vault job and strategy.
|
|
67
|
+
|
|
68
|
+
Sets up authentication and initializes the strategy with merged configuration.
|
|
69
|
+
"""
|
|
70
|
+
self._setup_strategy()
|
|
71
|
+
|
|
72
|
+
# Ensure auth token is set for API calls
|
|
73
|
+
if not self.clients._skip_auth:
|
|
74
|
+
is_api_key_auth = self._is_using_api_key()
|
|
75
|
+
|
|
76
|
+
if is_api_key_auth:
|
|
77
|
+
logger.debug("Using API key authentication")
|
|
78
|
+
if self.clients.auth:
|
|
79
|
+
await self.clients.auth._ensure_bearer_token()
|
|
80
|
+
else:
|
|
81
|
+
# Try to ensure bearer token is set, authenticate if needed
|
|
82
|
+
try:
|
|
83
|
+
if self.clients.auth:
|
|
84
|
+
await self.clients.auth._ensure_bearer_token()
|
|
85
|
+
except (PermissionError, Exception) as e:
|
|
86
|
+
if not isinstance(e, PermissionError):
|
|
87
|
+
logger.warning(
|
|
88
|
+
f"Authentication failed: {e}, trying OAuth fallback"
|
|
89
|
+
)
|
|
90
|
+
username = self.config.user.username
|
|
91
|
+
password = self.config.user.password
|
|
92
|
+
refresh_token = self.config.user.refresh_token
|
|
93
|
+
if refresh_token or (username and password):
|
|
94
|
+
await self.clients.authenticate(
|
|
95
|
+
username=username,
|
|
96
|
+
password=password,
|
|
97
|
+
refresh_token=refresh_token,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"Authentication required: provide api_key parameter for service account auth, "
|
|
102
|
+
"or username+password/refresh_token in config.json for personal access"
|
|
103
|
+
) from e
|
|
104
|
+
|
|
105
|
+
existing_cfg = dict(getattr(self.strategy, "config", {}) or {})
|
|
106
|
+
vault_cfg = dict(self.config.strategy_config or {})
|
|
107
|
+
merged_cfg = {**vault_cfg, **existing_cfg}
|
|
108
|
+
self.strategy.config = merged_cfg
|
|
109
|
+
self.strategy.clients = self.clients
|
|
110
|
+
await self.strategy.setup()
|
|
111
|
+
|
|
112
|
+
async def execute_strategy(self, action: str, **kwargs) -> dict[str, Any]:
|
|
113
|
+
"""Execute a strategy action (deposit, withdraw, update, status, partial_liquidate)"""
|
|
114
|
+
try:
|
|
115
|
+
if action == "deposit":
|
|
116
|
+
result = await self.strategy.deposit(**kwargs)
|
|
117
|
+
elif action == "withdraw":
|
|
118
|
+
result = await self.strategy.withdraw(**kwargs)
|
|
119
|
+
elif action == "update":
|
|
120
|
+
result = await self.strategy.update()
|
|
121
|
+
elif action == "status":
|
|
122
|
+
result = await self.strategy.status()
|
|
123
|
+
elif action == "partial_liquidate":
|
|
124
|
+
usd_value = kwargs.get("usd_value")
|
|
125
|
+
if usd_value is None:
|
|
126
|
+
result = (
|
|
127
|
+
False,
|
|
128
|
+
"usd_value parameter is required for partial_liquidate",
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
result = await self.strategy.partial_liquidate(usd_value)
|
|
132
|
+
else:
|
|
133
|
+
result = {"success": False, "message": f"Unknown action: {action}"}
|
|
134
|
+
|
|
135
|
+
await self.log(f"Strategy action '{action}' completed: {result}")
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
error_msg = f"Strategy action '{action}' failed: {str(e)}"
|
|
140
|
+
await self.log(error_msg)
|
|
141
|
+
await self.handle_error({"error": str(e), "action": action})
|
|
142
|
+
return {"success": False, "error": str(e)}
|
|
143
|
+
|
|
144
|
+
async def run_continuous(self, interval_seconds: int | None = None):
|
|
145
|
+
"""Run the strategy continuously at specified intervals"""
|
|
146
|
+
interval = interval_seconds or self.config.system.update_interval
|
|
147
|
+
logger.info(
|
|
148
|
+
f"Starting continuous execution for strategy: {self.strategy.name} with interval {interval}s"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
while True:
|
|
152
|
+
try:
|
|
153
|
+
await self.execute_strategy("update")
|
|
154
|
+
await asyncio.sleep(interval)
|
|
155
|
+
|
|
156
|
+
except asyncio.CancelledError:
|
|
157
|
+
logger.info("Continuous execution cancelled")
|
|
158
|
+
break
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Error in continuous execution: {str(e)}")
|
|
161
|
+
await asyncio.sleep(interval)
|
|
162
|
+
|
|
163
|
+
async def message_user(self, msg):
|
|
164
|
+
if msg is not None:
|
|
165
|
+
logger.info(f"notifying user: {msg}")
|
|
166
|
+
await self.clients.chat.send_msg_to_owner(
|
|
167
|
+
msg, "agent", self.clients.chat.session_id
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
async def log(self, msg: str):
|
|
171
|
+
"""Log messages for the job"""
|
|
172
|
+
logger.info(f"Job {self.job_id}: {msg}")
|
|
173
|
+
|
|
174
|
+
async def handle_error(self, error_data):
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
async def stop(self):
|
|
178
|
+
"""Stop the vault job and cleanup"""
|
|
179
|
+
if hasattr(self.strategy, "stop"):
|
|
180
|
+
await self.strategy.stop()
|
|
181
|
+
|
|
182
|
+
logger.info(f"Vault job {self.job_id} stopped")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
from pydantic import BaseModel, Field, validator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AdapterRequirement(BaseModel):
|
|
8
|
+
name: str = Field(
|
|
9
|
+
..., description="Adapter symbolic name (e.g., BALANCE, HYPERLIQUID)"
|
|
10
|
+
)
|
|
11
|
+
capabilities: list[str] = Field(default_factory=list)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AdapterManifest(BaseModel):
|
|
15
|
+
schema_version: str = Field(default="0.1")
|
|
16
|
+
entrypoint: str
|
|
17
|
+
capabilities: list[str] = Field(default_factory=list)
|
|
18
|
+
dependencies: list[str] = Field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
@validator("entrypoint")
|
|
21
|
+
def validate_entrypoint(cls, v: str) -> str:
|
|
22
|
+
if "." not in v:
|
|
23
|
+
raise ValueError("entrypoint must be a full import path")
|
|
24
|
+
return v
|
|
25
|
+
|
|
26
|
+
@validator("capabilities")
|
|
27
|
+
def validate_capabilities(cls, v: list[str]) -> list[str]:
|
|
28
|
+
if not v:
|
|
29
|
+
raise ValueError("capabilities cannot be empty")
|
|
30
|
+
return v
|
|
31
|
+
|
|
32
|
+
@validator("dependencies")
|
|
33
|
+
def validate_dependencies(cls, v: list[str]) -> list[str]:
|
|
34
|
+
if not v:
|
|
35
|
+
raise ValueError("dependencies cannot be empty")
|
|
36
|
+
return v
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StrategyManifest(BaseModel):
|
|
40
|
+
schema_version: str = Field(default="0.1")
|
|
41
|
+
entrypoint: str = Field(
|
|
42
|
+
...,
|
|
43
|
+
description="Python path to class, e.g. vaults.strategies.funding_rate_strategy.FundingRateStrategy",
|
|
44
|
+
)
|
|
45
|
+
name: str | None = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
description="Unique name identifier for this strategy instance. Used to look up dedicated wallet in wallets.json by label.",
|
|
48
|
+
)
|
|
49
|
+
permissions: dict[str, Any] = Field(default_factory=dict)
|
|
50
|
+
adapters: list[AdapterRequirement] = Field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
@validator("entrypoint")
|
|
53
|
+
def validate_entrypoint(cls, v: str) -> str:
|
|
54
|
+
if "." not in v:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"entrypoint must be a full import path to a Strategy class"
|
|
57
|
+
)
|
|
58
|
+
return v
|
|
59
|
+
|
|
60
|
+
@validator("permissions")
|
|
61
|
+
def validate_permissions(cls, v: dict) -> dict:
|
|
62
|
+
if "policy" not in v:
|
|
63
|
+
raise ValueError("permissions.policy is required")
|
|
64
|
+
if not v["policy"]:
|
|
65
|
+
raise ValueError("permissions.policy cannot be empty")
|
|
66
|
+
return v
|
|
67
|
+
|
|
68
|
+
@validator("adapters")
|
|
69
|
+
def validate_adapters(cls, v: list) -> list:
|
|
70
|
+
if not v:
|
|
71
|
+
raise ValueError("adapters cannot be empty")
|
|
72
|
+
return v
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load_adapter_manifest(path: str) -> AdapterManifest:
|
|
76
|
+
with open(path) as f:
|
|
77
|
+
data = yaml.safe_load(f)
|
|
78
|
+
return AdapterManifest(**data)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_strategy_manifest(path: str) -> StrategyManifest:
|
|
82
|
+
with open(path) as f:
|
|
83
|
+
data = yaml.safe_load(f)
|
|
84
|
+
return StrategyManifest(**data)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_manifest(path: str) -> StrategyManifest:
|
|
88
|
+
"""Legacy function for backward compatibility."""
|
|
89
|
+
return load_strategy_manifest(path)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def validate_manifest(manifest: StrategyManifest) -> None:
|
|
93
|
+
# Simple v0.1 rules: require at least one adapter and permissions.policy
|
|
94
|
+
if not manifest.adapters:
|
|
95
|
+
raise ValueError("Manifest must declare at least one adapter")
|
|
96
|
+
if "policy" not in manifest.permissions:
|
|
97
|
+
raise ValueError("Manifest.permissions must include 'policy'")
|
|
File without changes
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from web3 import AsyncWeb3
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TokenTxn(ABC):
|
|
8
|
+
"""Interface describing high-level EVM transaction builders."""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def build_send(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
token_id: str,
|
|
15
|
+
amount: float,
|
|
16
|
+
from_address: str,
|
|
17
|
+
to_address: str,
|
|
18
|
+
token_info: dict[str, Any] | None = None,
|
|
19
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
20
|
+
"""Build raw transaction data for sending tokens."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def build_erc20_approve(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
chain_id: int,
|
|
27
|
+
token_address: str,
|
|
28
|
+
from_address: str,
|
|
29
|
+
spender: str,
|
|
30
|
+
amount: int,
|
|
31
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
32
|
+
"""Build raw ERC20 approve transaction data."""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
async def read_erc20_allowance(
|
|
36
|
+
self, chain: Any, token_address: str, from_address: str, spender_address: str
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""Read allowance granted for a spender."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EvmTxn(ABC):
|
|
42
|
+
"""
|
|
43
|
+
Abstract base class for wallet providers.
|
|
44
|
+
|
|
45
|
+
This interface abstracts all blockchain interactions needed by adapters so the
|
|
46
|
+
rest of the vaults codebase never touches raw web3 primitives. Implementations
|
|
47
|
+
are responsible for RPC resolution, gas estimation, signing, broadcasting and
|
|
48
|
+
transaction confirmations.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
async def get_balance(
|
|
53
|
+
self,
|
|
54
|
+
address: str,
|
|
55
|
+
token_address: str | None,
|
|
56
|
+
chain_id: int,
|
|
57
|
+
) -> tuple[bool, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Get balance for an address.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
address: Address to query balance for
|
|
63
|
+
token_address: ERC20 token address, or None for native token
|
|
64
|
+
chain_id: Chain ID
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (success, balance_integer_or_error_message)
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
async def approve_token(
|
|
73
|
+
self,
|
|
74
|
+
token_address: str,
|
|
75
|
+
spender: str,
|
|
76
|
+
amount: int,
|
|
77
|
+
from_address: str,
|
|
78
|
+
chain_id: int,
|
|
79
|
+
wait_for_receipt: bool = True,
|
|
80
|
+
timeout: int = 120,
|
|
81
|
+
) -> tuple[bool, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Approve a spender to spend tokens on behalf of from_address.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
token_address: ERC20 token contract address
|
|
87
|
+
spender: Address being approved to spend tokens
|
|
88
|
+
amount: Amount to approve (in token units, not human-readable)
|
|
89
|
+
from_address: Address approving the tokens
|
|
90
|
+
chain_id: Chain ID
|
|
91
|
+
wait_for_receipt: Whether to wait for the transaction receipt
|
|
92
|
+
timeout: Receipt timeout in seconds
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Tuple of (success, transaction_result_dict_or_error_message)
|
|
96
|
+
Transaction result should include 'tx_hash' and optionally 'receipt'
|
|
97
|
+
"""
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
async def broadcast_transaction(
|
|
102
|
+
self,
|
|
103
|
+
transaction: dict[str, Any],
|
|
104
|
+
*,
|
|
105
|
+
wait_for_receipt: bool = True,
|
|
106
|
+
timeout: int = 120,
|
|
107
|
+
) -> tuple[bool, Any]:
|
|
108
|
+
"""
|
|
109
|
+
Sign and broadcast a transaction dict.
|
|
110
|
+
|
|
111
|
+
Providers must handle gas estimation, gas pricing, nonce selection, signing
|
|
112
|
+
and submission internally so callers can simply pass the transaction data.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
transaction: Dictionary describing the transaction (to, data, value, etc.)
|
|
116
|
+
wait_for_receipt: Whether to wait for the transaction receipt
|
|
117
|
+
timeout: Receipt timeout in seconds
|
|
118
|
+
"""
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
async def transaction_succeeded(
|
|
123
|
+
self, tx_hash: str, chain_id: int, timeout: int = 120
|
|
124
|
+
) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Check if a transaction hash succeeded on-chain.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
tx_hash: Transaction hash to inspect
|
|
130
|
+
chain_id: Chain ID where the transaction was broadcast
|
|
131
|
+
timeout: Maximum seconds to wait for a receipt
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Boolean indicating whether the transaction completed successfully.
|
|
135
|
+
"""
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
def get_web3(self, chain_id: int) -> AsyncWeb3:
|
|
140
|
+
"""
|
|
141
|
+
Return an AsyncWeb3 instance configured for the given chain.
|
|
142
|
+
|
|
143
|
+
Implementations may create new instances per call or pull from an internal
|
|
144
|
+
cache, but they must document whether the caller is responsible for closing
|
|
145
|
+
the underlying HTTP session.
|
|
146
|
+
"""
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Web3Service(ABC):
|
|
151
|
+
"""Facade that exposes low-level wallet access and higher-level EVM helpers."""
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
@abstractmethod
|
|
155
|
+
def evm_transactions(self) -> EvmTxn:
|
|
156
|
+
"""Return the wallet provider responsible for RPC, signing, and broadcasting."""
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
@abstractmethod
|
|
160
|
+
def token_transactions(self) -> TokenTxn:
|
|
161
|
+
"""Returns TokenTxn, for sends and swaps of any token"""
|
|
162
|
+
|
|
163
|
+
async def broadcast_transaction(
|
|
164
|
+
self,
|
|
165
|
+
transaction: dict[str, Any],
|
|
166
|
+
*,
|
|
167
|
+
wait_for_receipt: bool = True,
|
|
168
|
+
timeout: int = 120,
|
|
169
|
+
) -> tuple[bool, Any]:
|
|
170
|
+
"""Proxy convenience wrapper to underlying wallet provider."""
|
|
171
|
+
return await self.evm_transactions.broadcast_transaction(
|
|
172
|
+
transaction, wait_for_receipt=wait_for_receipt, timeout=timeout
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def get_web3(self, chain_id: int):
|
|
176
|
+
"""Expose underlying web3 provider for ABI encoding helpers."""
|
|
177
|
+
return self.evm_transactions.get_web3(chain_id)
|