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.

Files changed (115) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +394 -0
  2. wayfinder_paths/__init__.py +21 -0
  3. wayfinder_paths/config.example.json +20 -0
  4. wayfinder_paths/conftest.py +31 -0
  5. wayfinder_paths/core/__init__.py +13 -0
  6. wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
  7. wayfinder_paths/core/adapters/__init__.py +5 -0
  8. wayfinder_paths/core/adapters/base.py +5 -0
  9. wayfinder_paths/core/clients/AuthClient.py +83 -0
  10. wayfinder_paths/core/clients/BRAPClient.py +90 -0
  11. wayfinder_paths/core/clients/ClientManager.py +231 -0
  12. wayfinder_paths/core/clients/HyperlendClient.py +151 -0
  13. wayfinder_paths/core/clients/LedgerClient.py +222 -0
  14. wayfinder_paths/core/clients/PoolClient.py +96 -0
  15. wayfinder_paths/core/clients/SimulationClient.py +180 -0
  16. wayfinder_paths/core/clients/TokenClient.py +73 -0
  17. wayfinder_paths/core/clients/TransactionClient.py +47 -0
  18. wayfinder_paths/core/clients/WalletClient.py +90 -0
  19. wayfinder_paths/core/clients/WayfinderClient.py +258 -0
  20. wayfinder_paths/core/clients/__init__.py +48 -0
  21. wayfinder_paths/core/clients/protocols.py +295 -0
  22. wayfinder_paths/core/clients/sdk_example.py +115 -0
  23. wayfinder_paths/core/config.py +369 -0
  24. wayfinder_paths/core/constants/__init__.py +26 -0
  25. wayfinder_paths/core/constants/base.py +25 -0
  26. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  27. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  28. wayfinder_paths/core/engine/VaultJob.py +182 -0
  29. wayfinder_paths/core/engine/__init__.py +5 -0
  30. wayfinder_paths/core/engine/manifest.py +97 -0
  31. wayfinder_paths/core/services/__init__.py +0 -0
  32. wayfinder_paths/core/services/base.py +177 -0
  33. wayfinder_paths/core/services/local_evm_txn.py +429 -0
  34. wayfinder_paths/core/services/local_token_txn.py +231 -0
  35. wayfinder_paths/core/services/web3_service.py +45 -0
  36. wayfinder_paths/core/settings.py +61 -0
  37. wayfinder_paths/core/strategies/Strategy.py +183 -0
  38. wayfinder_paths/core/strategies/__init__.py +5 -0
  39. wayfinder_paths/core/strategies/base.py +7 -0
  40. wayfinder_paths/core/utils/__init__.py +1 -0
  41. wayfinder_paths/core/utils/evm_helpers.py +165 -0
  42. wayfinder_paths/core/utils/wallets.py +77 -0
  43. wayfinder_paths/core/wallets/README.md +91 -0
  44. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  45. wayfinder_paths/core/wallets/__init__.py +7 -0
  46. wayfinder_paths/run_strategy.py +409 -0
  47. wayfinder_paths/scripts/__init__.py +0 -0
  48. wayfinder_paths/scripts/create_strategy.py +181 -0
  49. wayfinder_paths/scripts/make_wallets.py +160 -0
  50. wayfinder_paths/scripts/validate_manifests.py +213 -0
  51. wayfinder_paths/tests/__init__.py +0 -0
  52. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  53. wayfinder_paths/tests/test_test_coverage.py +212 -0
  54. wayfinder_paths/tests/test_utils.py +64 -0
  55. wayfinder_paths/vaults/__init__.py +0 -0
  56. wayfinder_paths/vaults/adapters/__init__.py +0 -0
  57. wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
  58. wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
  59. wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
  60. wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
  61. wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
  62. wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
  63. wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
  64. wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
  65. wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
  66. wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
  67. wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
  68. wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
  69. wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
  70. wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
  71. wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
  72. wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
  73. wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
  74. wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
  75. wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
  76. wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
  77. wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
  78. wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
  79. wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
  80. wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
  81. wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
  82. wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
  83. wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
  84. wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
  85. wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
  86. wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
  87. wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
  88. wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
  89. wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
  90. wayfinder_paths/vaults/strategies/__init__.py +0 -0
  91. wayfinder_paths/vaults/strategies/config.py +85 -0
  92. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
  93. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
  94. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  95. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
  96. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
  97. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
  98. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
  99. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  100. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
  101. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
  102. wayfinder_paths/vaults/templates/adapter/README.md +105 -0
  103. wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
  104. wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
  105. wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
  106. wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
  107. wayfinder_paths/vaults/templates/strategy/README.md +152 -0
  108. wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
  109. wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
  110. wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
  111. wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
  112. wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
  113. wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
  114. wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
  115. 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,5 @@
1
+ """Core Engine Module"""
2
+
3
+ from .VaultJob import VaultJob
4
+
5
+ __all__ = ["VaultJob", "GLOBAL_IMPORTS"]
@@ -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)