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,101 @@
|
|
|
1
|
+
# Token Adapter
|
|
2
|
+
|
|
3
|
+
A Wayfinder adapter that wraps the `_get_token_via_api` method for fetching token data via HeadlessAPIViewSet endpoints. This adapter supports both address and token_id lookups.
|
|
4
|
+
|
|
5
|
+
## Capabilities
|
|
6
|
+
|
|
7
|
+
- `token.read`: Retrieve token information by address or token ID
|
|
8
|
+
|
|
9
|
+
## Configuration
|
|
10
|
+
|
|
11
|
+
The adapter uses the TokenClient which automatically handles authentication and API configuration through the Wayfinder settings. No additional configuration is required.
|
|
12
|
+
|
|
13
|
+
The TokenClient will automatically:
|
|
14
|
+
- Use the WAYFINDER_API_URL from settings
|
|
15
|
+
- Handle authentication via environment variables or config.json
|
|
16
|
+
- Manage token refresh and retry logic
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Initialize the Adapter
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from wayfinder_paths.vaults.adapters.token_adapter.adapter import TokenAdapter
|
|
24
|
+
|
|
25
|
+
# No configuration needed - uses TokenClient with automatic settings
|
|
26
|
+
adapter = TokenAdapter()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Get Token Metadata
|
|
30
|
+
|
|
31
|
+
Both contract addresses and token ids are supported with the same method. Pass whichever identifier you have:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
success, data = await adapter.get_token("0x1234...") # by address
|
|
35
|
+
# or
|
|
36
|
+
success, data = await adapter.get_token("usd-coin-base") # by token id
|
|
37
|
+
|
|
38
|
+
if success:
|
|
39
|
+
print(data)
|
|
40
|
+
else:
|
|
41
|
+
print(f"Error: {data}")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Get Token Price
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
success, data = await adapter.get_token_price("token-123")
|
|
48
|
+
if success:
|
|
49
|
+
print(f"Price: ${data['current_price']}")
|
|
50
|
+
print(f"24h Change: {data['price_change_percentage_24h']}%")
|
|
51
|
+
else:
|
|
52
|
+
print(f"Error: {data}")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Get Gas Token
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
success, data = await adapter.get_gas_token("base")
|
|
59
|
+
if success:
|
|
60
|
+
print(f"Gas token: {data['symbol']} - {data['name']}")
|
|
61
|
+
print(f"Address: {data['address']}")
|
|
62
|
+
else:
|
|
63
|
+
print(f"Error: {data}")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Get Token (Flexible)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Try by address first, then by token_id
|
|
70
|
+
success, data = await adapter.get_token(address="0x1234...", token_id="token-123")
|
|
71
|
+
if success:
|
|
72
|
+
print(f"Token data: {data}")
|
|
73
|
+
else:
|
|
74
|
+
print(f"Error: {data}")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## API Endpoints
|
|
78
|
+
|
|
79
|
+
The adapter uses the following existing public API endpoints:
|
|
80
|
+
|
|
81
|
+
1. **Token Details** (by address): `GET /public/tokens/detail/?query={address}&get_chart=false`
|
|
82
|
+
2. **Token by ID**: `GET /public/tokens/lookup/?token_id={token_id}`
|
|
83
|
+
|
|
84
|
+
## Error Handling
|
|
85
|
+
|
|
86
|
+
All methods return a tuple of `(success: bool, data: Any)` where:
|
|
87
|
+
- `success` indicates whether the operation was successful
|
|
88
|
+
- `data` contains either the token information (on success) or an error message (on failure)
|
|
89
|
+
|
|
90
|
+
## Health Check
|
|
91
|
+
|
|
92
|
+
The adapter includes a health check that tests connectivity to the API:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
health = await adapter.health_check()
|
|
96
|
+
print(f"Status: {health['status']}")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Examples
|
|
100
|
+
|
|
101
|
+
See `examples.json` for more detailed usage examples.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
4
|
+
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TokenAdapter(BaseAdapter):
|
|
8
|
+
"""
|
|
9
|
+
Token adapter that wraps the _get_token_via_api method for fetching token data
|
|
10
|
+
via HeadlessAPIViewSet endpoints. Supports both address and token_id lookups.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
adapter_type: str = "TOKEN"
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
config: dict[str, Any] | None = None,
|
|
18
|
+
token_client: TokenClient | None = None,
|
|
19
|
+
):
|
|
20
|
+
super().__init__("token_adapter", config)
|
|
21
|
+
self.token_client = token_client or TokenClient()
|
|
22
|
+
|
|
23
|
+
async def get_token(self, query: str) -> tuple[bool, Any]:
|
|
24
|
+
"""
|
|
25
|
+
Get token data by address using the token-details endpoint.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
address: Token contract address
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Tuple of (success, data) where data is the token information or error message
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
data = await self.token_client.get_token_details(query)
|
|
35
|
+
if not data:
|
|
36
|
+
return (False, f"No token found for: {query}")
|
|
37
|
+
return (True, data)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
self.logger.error(f"Error getting token by query {query}: {e}")
|
|
40
|
+
return (False, str(e))
|
|
41
|
+
|
|
42
|
+
async def get_token_price(self, token_id: str) -> tuple[bool, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Get token price by token ID or address using the token-details endpoint.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
token_id: Token identifier or address
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Tuple of (success, data) where data is the price information or error message
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
data = await self.token_client.get_token_details(token_id)
|
|
54
|
+
if not data:
|
|
55
|
+
return (False, f"No token found for: {token_id}")
|
|
56
|
+
|
|
57
|
+
# Extract just the price information
|
|
58
|
+
price_data = {
|
|
59
|
+
"current_price": data.get("current_price", 0.0),
|
|
60
|
+
"price_change_24h": data.get("price_change_24h", 0.0),
|
|
61
|
+
"price_change_percentage_24h": data.get(
|
|
62
|
+
"price_change_percentage_24h", 0.0
|
|
63
|
+
),
|
|
64
|
+
"market_cap": data.get("market_cap", 0),
|
|
65
|
+
"total_volume": data.get("total_volume", 0),
|
|
66
|
+
"symbol": data.get("symbol", ""),
|
|
67
|
+
"name": data.get("name", ""),
|
|
68
|
+
"address": data.get("address", ""),
|
|
69
|
+
}
|
|
70
|
+
return (True, price_data)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
self.logger.error(f"Error getting token price for {token_id}: {e}")
|
|
73
|
+
return (False, str(e))
|
|
74
|
+
|
|
75
|
+
async def get_gas_token(self, chain_code: str) -> tuple[bool, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Get gas token for a given chain code.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
chain_code: Chain code (e.g., "base", "ethereum")
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Tuple of (success, data) where data is the gas token information or error message
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
data = await self.token_client.get_gas_token(chain_code)
|
|
87
|
+
if not data:
|
|
88
|
+
return (False, f"No gas token found for chain: {chain_code}")
|
|
89
|
+
return (True, data)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
self.logger.error(f"Error getting gas token for chain {chain_code}: {e}")
|
|
92
|
+
return (False, str(e))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"basic_usage": {
|
|
3
|
+
"description": "Basic usage of TokenAdapter to get token information",
|
|
4
|
+
"code": "from vaults.adapters.token_adapter.adapter import TokenAdapter\n\n# Initialize adapter (no config needed)\nadapter = TokenAdapter()\n\n# Get token by address\nsuccess, data = await adapter.get_token_by_address(\"0x1234567890abcdef1234567890abcdef12345678\")\nif success:\n print(f\"Token symbol: {data.get('symbol')}\")\n print(f\"Token name: {data.get('name')}\")\n print(f\"Decimals: {data.get('decimals')}\")\nelse:\n print(f\"Error: {data}\")"
|
|
5
|
+
},
|
|
6
|
+
"get_by_token_id": {
|
|
7
|
+
"description": "Get token information using token ID",
|
|
8
|
+
"code": "from vaults.adapters.token_adapter.adapter import TokenAdapter\n\n# Initialize adapter (no config needed)\nadapter = TokenAdapter()\n\n# Get token by ID\nsuccess, data = await adapter.get_token_by_id(\"token-12345\")\nif success:\n print(f\"Token data: {data}\")\nelse:\n print(f\"Error: {data}\")"
|
|
9
|
+
},
|
|
10
|
+
"flexible_lookup": {
|
|
11
|
+
"description": "Use flexible get_token method that tries both address and token_id",
|
|
12
|
+
"code": "from vaults.adapters.token_adapter.adapter import TokenAdapter\n\n# Initialize adapter (no config needed)\nadapter = TokenAdapter()\n\n# Try both address and token_id\nsuccess, data = await adapter.get_token(\n address=\"0x1234567890abcdef1234567890abcdef12345678\",\n token_id=\"token-12345\"\n)\nif success:\n print(f\"Found token: {data}\")\nelse:\n print(f\"Token not found: {data}\")"
|
|
13
|
+
},
|
|
14
|
+
"error_handling": {
|
|
15
|
+
"description": "Proper error handling for various scenarios",
|
|
16
|
+
"code": "from vaults.adapters.token_adapter.adapter import TokenAdapter\n\n# Initialize adapter (no config needed)\nadapter = TokenAdapter()\n\n# Handle missing parameters\ntry:\n success, data = await adapter.get_token()\n if not success:\n print(f\"Error: {data}\")\nexcept Exception as e:\n print(f\"Unexpected error: {e}\")\n\n# Handle API errors\nsuccess, data = await adapter.get_token_by_address(\"invalid-address\")\nif not success:\n print(f\"API error: {data}\")\nelse:\n print(f\"Token found: {data}\")"
|
|
17
|
+
},
|
|
18
|
+
"health_check": {
|
|
19
|
+
"description": "Check adapter health and connectivity",
|
|
20
|
+
"code": "from vaults.adapters.token_adapter.adapter import TokenAdapter\n\n# Initialize adapter (no config needed)\nadapter = TokenAdapter()\n\n# Check health\nhealth = await adapter.health_check()\nprint(f\"Adapter status: {health['status']}\")\nprint(f\"Connected: {health['connected']}\")\nprint(f\"Adapter type: {health['adapter']}\")\n\nif health['status'] == 'healthy':\n print(\"Adapter is ready to use\")\nelse:\n print(f\"Adapter has issues: {health.get('error', 'Unknown error')}\")"
|
|
21
|
+
},
|
|
22
|
+
"batch_operations": {
|
|
23
|
+
"description": "Perform multiple token lookups efficiently",
|
|
24
|
+
"code": "from vaults.adapters.token_adapter.adapter import TokenAdapter\nimport asyncio\n\n# Initialize adapter (no config needed)\nadapter = TokenAdapter()\n\n# List of token addresses to lookup\ntoken_addresses = [\n \"0x1234567890abcdef1234567890abcdef12345678\",\n \"0xabcdef1234567890abcdef1234567890abcdef12\",\n \"0x9876543210fedcba9876543210fedcba98765432\"\n]\n\n# Batch lookup\ntoken_data = {}\nfor address in token_addresses:\n success, data = await adapter.get_token_by_address(address)\n if success:\n token_data[address] = data\n else:\n print(f\"Failed to get token for {address}: {data}\")\n\nprint(f\"Successfully retrieved {len(token_data)} tokens\")\nfor address, data in token_data.items():\n print(f\"{address}: {data.get('symbol', 'Unknown')} - {data.get('name', 'Unknown')}\")"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.vaults.adapters.token_adapter.adapter import TokenAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestTokenAdapter:
|
|
9
|
+
"""Test cases for TokenAdapter"""
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def adapter(self):
|
|
13
|
+
return TokenAdapter()
|
|
14
|
+
|
|
15
|
+
def test_init_with_default_config(self):
|
|
16
|
+
"""Test adapter initialization with default config"""
|
|
17
|
+
adapter = TokenAdapter()
|
|
18
|
+
assert adapter.adapter_type == "TOKEN"
|
|
19
|
+
assert adapter.token_client is not None
|
|
20
|
+
|
|
21
|
+
@pytest.mark.asyncio
|
|
22
|
+
async def test_get_token_success(self, adapter):
|
|
23
|
+
"""Test successful token retrieval by address"""
|
|
24
|
+
mock_token_data = {
|
|
25
|
+
"address": "0x1234...",
|
|
26
|
+
"symbol": "TEST",
|
|
27
|
+
"name": "Test Token",
|
|
28
|
+
"decimals": 18,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
with patch.object(
|
|
32
|
+
adapter.token_client, "get_token_details", return_value=mock_token_data
|
|
33
|
+
):
|
|
34
|
+
success, data = await adapter.get_token("0x1234...")
|
|
35
|
+
|
|
36
|
+
assert success is True
|
|
37
|
+
assert data == mock_token_data
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_get_token_not_found(self, adapter):
|
|
41
|
+
"""Test token not found by address"""
|
|
42
|
+
with patch.object(adapter.token_client, "get_token_details", return_value=None):
|
|
43
|
+
success, data = await adapter.get_token("0x1234...")
|
|
44
|
+
|
|
45
|
+
assert success is False
|
|
46
|
+
assert "No token found for address" in data
|
|
47
|
+
|
|
48
|
+
@pytest.mark.asyncio
|
|
49
|
+
async def test_get_token_flexible_success(self, adapter):
|
|
50
|
+
"""Test flexible token retrieval with both address and token_id"""
|
|
51
|
+
mock_token_data = {"address": "0x1234...", "symbol": "TEST"}
|
|
52
|
+
|
|
53
|
+
with patch.object(
|
|
54
|
+
adapter.token_client, "get_token_details", return_value=mock_token_data
|
|
55
|
+
):
|
|
56
|
+
success, data = await adapter.get_token(
|
|
57
|
+
address="0x1234...", token_id="token-123"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
assert success is True
|
|
61
|
+
assert data == mock_token_data
|
|
62
|
+
|
|
63
|
+
@pytest.mark.asyncio
|
|
64
|
+
async def test_get_token_no_parameters(self, adapter):
|
|
65
|
+
"""Test get_token with no parameters raises error"""
|
|
66
|
+
success, data = await adapter.get_token()
|
|
67
|
+
|
|
68
|
+
assert success is False
|
|
69
|
+
assert "Either address or token_id must be provided" in data
|
|
70
|
+
|
|
71
|
+
def test_adapter_type(self, adapter):
|
|
72
|
+
"""Test adapter has adapter_type"""
|
|
73
|
+
assert adapter.adapter_type == "TOKEN"
|
|
74
|
+
|
|
75
|
+
@pytest.mark.asyncio
|
|
76
|
+
async def test_get_token_price_success(self, adapter):
|
|
77
|
+
"""Test successful token price retrieval"""
|
|
78
|
+
mock_token_data = {
|
|
79
|
+
"current_price": 1.50,
|
|
80
|
+
"price_change_24h": 0.05,
|
|
81
|
+
"price_change_percentage_24h": 3.45,
|
|
82
|
+
"market_cap": 1000000,
|
|
83
|
+
"total_volume": 50000,
|
|
84
|
+
"symbol": "TEST",
|
|
85
|
+
"name": "Test Token",
|
|
86
|
+
"address": "0x1234...",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
with patch.object(
|
|
90
|
+
adapter.token_client, "get_token_details", return_value=mock_token_data
|
|
91
|
+
):
|
|
92
|
+
success, data = await adapter.get_token_price("test-token")
|
|
93
|
+
|
|
94
|
+
assert success is True
|
|
95
|
+
assert data["current_price"] == 1.50
|
|
96
|
+
assert data["symbol"] == "TEST"
|
|
97
|
+
assert data["name"] == "Test Token"
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_get_token_price_not_found(self, adapter):
|
|
101
|
+
"""Test token price not found"""
|
|
102
|
+
with patch.object(adapter.token_client, "get_token_details", return_value=None):
|
|
103
|
+
success, data = await adapter.get_token_price("invalid-token")
|
|
104
|
+
|
|
105
|
+
assert success is False
|
|
106
|
+
assert "No token found for" in data
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_get_gas_token_success(self, adapter):
|
|
110
|
+
"""Test successful gas token retrieval"""
|
|
111
|
+
mock_gas_token_data = {
|
|
112
|
+
"id": "ethereum-base",
|
|
113
|
+
"symbol": "ETH",
|
|
114
|
+
"name": "Ethereum",
|
|
115
|
+
"address": "0x0000000000000000000000000000000000000000",
|
|
116
|
+
"decimals": 18,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
with patch.object(
|
|
120
|
+
adapter.token_client, "get_gas_token", return_value=mock_gas_token_data
|
|
121
|
+
):
|
|
122
|
+
success, data = await adapter.get_gas_token("base")
|
|
123
|
+
|
|
124
|
+
assert success is True
|
|
125
|
+
assert data["symbol"] == "ETH"
|
|
126
|
+
assert data["name"] == "Ethereum"
|
|
127
|
+
|
|
128
|
+
@pytest.mark.asyncio
|
|
129
|
+
async def test_get_gas_token_not_found(self, adapter):
|
|
130
|
+
"""Test gas token not found"""
|
|
131
|
+
with patch.object(adapter.token_client, "get_gas_token", return_value=None):
|
|
132
|
+
success, data = await adapter.get_gas_token("invalid-chain")
|
|
133
|
+
|
|
134
|
+
assert success is False
|
|
135
|
+
assert "No gas token found for chain" in data
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration for community strategies
|
|
3
|
+
Each strategy can define its own configuration parameters
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# Funding Rate Strategy Configuration
|
|
9
|
+
FUNDING_RATE_CONFIG = {
|
|
10
|
+
"min_deposit": 30, # USDC
|
|
11
|
+
"lookback_days": 90,
|
|
12
|
+
"confidence": 0.9999,
|
|
13
|
+
"min_open_interest": 50_000,
|
|
14
|
+
"min_daily_volume": 100_000,
|
|
15
|
+
"max_leverage": 3,
|
|
16
|
+
"liquidation_threshold": 0.75,
|
|
17
|
+
"rebalance_threshold": 0.05,
|
|
18
|
+
"hyperliquid_system_address": "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7",
|
|
19
|
+
"supported_chains": ["arbitrum"],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Stablecoin Yield Strategy Configuration
|
|
24
|
+
STABLECOIN_YIELD_CONFIG = {
|
|
25
|
+
"min_deposit": 50, # USDC
|
|
26
|
+
"min_tvl": 1_000_000, # $1M minimum TVL for safety
|
|
27
|
+
"min_apy": 0.01, # 1% minimum APY
|
|
28
|
+
"rebalance_days": 7, # Days until rebalance is profitable
|
|
29
|
+
"search_depth": 10, # Number of pools to evaluate
|
|
30
|
+
"gas_buffer": 0.001, # ETH for gas
|
|
31
|
+
"supported_chains": ["base", "arbitrum"],
|
|
32
|
+
"supported_tokens": ["USDC", "DAI", "USDT"],
|
|
33
|
+
"excluded_protocols": [], # Protocols to avoid
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Moonwell wstETH Loop Strategy Configuration (example for advanced strategies)
|
|
38
|
+
MOONWELL_LOOP_CONFIG = {
|
|
39
|
+
"min_deposit": 200, # USDC
|
|
40
|
+
"max_loops": 30,
|
|
41
|
+
"leverage_limit": 10,
|
|
42
|
+
"contracts": {
|
|
43
|
+
"m_usdc": "0xedc817a28e8b93b03976fbd4a3ddbc9f7d176c22",
|
|
44
|
+
"m_weth": "0x628ff693426583D9a7FB391E54366292F509D457",
|
|
45
|
+
"m_wsteth": "0x627fe393bc6edda28e99ae648fd6ff362514304b",
|
|
46
|
+
"reward_distributor": "0xe9005b078701e2a0948d2eac43010d35870ad9d2",
|
|
47
|
+
"comptroller": "0xfbb21d0380bee3312b33c4353c8936a0f13ef26c",
|
|
48
|
+
},
|
|
49
|
+
"supported_chains": ["base"],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Global adapter configurations
|
|
54
|
+
ADAPTER_CONFIGS = {
|
|
55
|
+
"hyperliquid": {
|
|
56
|
+
"api_url": "https://api.hyperliquid.xyz",
|
|
57
|
+
"testnet_url": "https://api.hyperliquid-testnet.xyz",
|
|
58
|
+
"rate_limit": 10, # requests per second
|
|
59
|
+
"timeout": 30, # seconds
|
|
60
|
+
"slippage": 0.05, # 5% default slippage for market orders
|
|
61
|
+
},
|
|
62
|
+
"enso": {
|
|
63
|
+
"router_address": "0xF75584eF6673aD213a685a1B58Cc0330B8eA22Cf",
|
|
64
|
+
"supported_chains": ["ethereum", "base", "arbitrum", "polygon"],
|
|
65
|
+
},
|
|
66
|
+
"moonwell": {
|
|
67
|
+
"supported_chains": ["base"],
|
|
68
|
+
"protocol_fee": 0.001, # 0.1%
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_strategy_config(strategy_name: str) -> dict[str, Any]:
|
|
74
|
+
"""Get configuration for a specific strategy"""
|
|
75
|
+
configs = {
|
|
76
|
+
"funding_rate": FUNDING_RATE_CONFIG,
|
|
77
|
+
"stablecoin_yield": STABLECOIN_YIELD_CONFIG,
|
|
78
|
+
"moonwell_loop": MOONWELL_LOOP_CONFIG,
|
|
79
|
+
}
|
|
80
|
+
return configs.get(strategy_name.lower(), {})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_adapter_config(adapter_name: str) -> dict[str, Any]:
|
|
84
|
+
"""Get configuration for a specific adapter"""
|
|
85
|
+
return ADAPTER_CONFIGS.get(adapter_name.lower(), {})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Hyperlend Stable Yield Strategy
|
|
2
|
+
|
|
3
|
+
- Entrypoint: `vaults.strategies.hyperlend_stable_yield_strategy.strategy.HyperlendStableYieldStrategy`
|
|
4
|
+
- Manifest: `manifest.yaml`
|
|
5
|
+
- Examples: `examples.json`
|
|
6
|
+
- Tests: `test_strategy.py`
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
Allocates USDT0 on HyperEVM across HyperLend stablecoin markets. The strategy:
|
|
11
|
+
|
|
12
|
+
1. Pulls USDT0 (plus a configurable HYPE gas buffer) from the main wallet into the vault wallet.
|
|
13
|
+
2. Samples HyperLend hourly rate history, applies a bootstrap tournament (horizon = 6h, blocks = 6h, 4,000 trials, 7-day half-life) to estimate which stablecoin should outperform.
|
|
14
|
+
3. Tops up the small HYPE gas buffer if needed, swaps USDT0 into the target stablecoin, and supplies it to HyperLend.
|
|
15
|
+
4. Enforces a hysteresis rotation policy so minor APY noise does not churn capital.
|
|
16
|
+
|
|
17
|
+
## Policy
|
|
18
|
+
|
|
19
|
+
The manifest policy simply locks transactions to the vault wallet ID:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
(wallet.id == 'FORMAT_WALLET_ID')
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Key parameters
|
|
26
|
+
|
|
27
|
+
- `MIN_USDT0_DEPOSIT_AMOUNT = 1`
|
|
28
|
+
- `GAS_MAXIMUM = 0.1` HYPE (max accepted per deposit)
|
|
29
|
+
- `HORIZON_HOURS = 6`, `BLOCK_LEN = 6`, `TRIALS = 4000`
|
|
30
|
+
- `HYSTERESIS_DWELL_HOURS = 168`, `HYSTERESIS_Z = 1.15`
|
|
31
|
+
- `ROTATION_COOLDOWN = 168 hours`
|
|
32
|
+
- `APY_REBALANCE_THRESHOLD = 0.0035` (35 bps edge required to rotate when not short-circuiting)
|
|
33
|
+
- `MIN_STABLE_SWAP_TOKENS = 1e-3` → dust threshold when sweeping balances
|
|
34
|
+
|
|
35
|
+
## Adapters used
|
|
36
|
+
|
|
37
|
+
- `BalanceAdapter` for token/pool balances and orchestrating wallet transfers with ledger tracking.
|
|
38
|
+
- `TokenAdapter` for metadata (USDT0, HYPE, wrapping info).
|
|
39
|
+
- `LedgerAdapter` for net deposit + rotation history.
|
|
40
|
+
- `BRAPAdapter` to source quotes/swap stablecoins.
|
|
41
|
+
- `HyperlendAdapter` for asset views, lend/withdraw ops, supply caps.
|
|
42
|
+
- `LocalTokenTxnService` via `DefaultWeb3Service` for low-level sends/approvals leveraged by the adapters.
|
|
43
|
+
|
|
44
|
+
## Actions
|
|
45
|
+
|
|
46
|
+
### Deposit
|
|
47
|
+
|
|
48
|
+
- Validates USDT0 and HYPE balances in the main wallet.
|
|
49
|
+
- Transfers HYPE into the vault wallet when a top-up is required, ensuring the vault maintains the configured buffer.
|
|
50
|
+
- Moves USDT0 from the main wallet into the vault wallet through `BalanceAdapter.move_from_main_wallet_to_vault_wallet`.
|
|
51
|
+
- Clears cached asset snapshots so the next update starts from on-chain reality.
|
|
52
|
+
|
|
53
|
+
### Update
|
|
54
|
+
|
|
55
|
+
- Refreshes HyperLend asset snapshots, calculates tournament winners, and filters markets that respect supply caps + buffer requirements.
|
|
56
|
+
- Reads rotation history through `LedgerAdapter.get_vault_latest_transactions` to enforce the cooldown (unless the short-circuit policy is triggered).
|
|
57
|
+
- If a new asset wins the tournament and passes hysteresis checks, BRAP quotes are fetched and executed to rotate into the better performer.
|
|
58
|
+
- Sweeps residual stable balances, lends via `HyperlendAdapter`, and records ledger operations.
|
|
59
|
+
|
|
60
|
+
### Status
|
|
61
|
+
|
|
62
|
+
`_status()` returns:
|
|
63
|
+
|
|
64
|
+
- `portfolio_value`: active lend balance (converted to float),
|
|
65
|
+
- `net_deposit`: fetched from `LedgerAdapter`,
|
|
66
|
+
- `strategy_status`: includes current lent asset, APY, idle balances, and tournament projections.
|
|
67
|
+
|
|
68
|
+
### Withdraw
|
|
69
|
+
|
|
70
|
+
- Unwinds existing HyperLend positions, swaps back to USDT0 when necessary, returns USDT0 and residual HYPE to the main wallet via `BalanceAdapter`, and clears cached state.
|
|
71
|
+
|
|
72
|
+
## Running locally
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Install dependencies
|
|
76
|
+
poetry install
|
|
77
|
+
|
|
78
|
+
# Generate default + vault wallets (writes wallets.json)
|
|
79
|
+
poetry run python wayfinder_paths/scripts/make_wallets.py --default --vault
|
|
80
|
+
|
|
81
|
+
# Copy config and edit credentials (or rely on env vars)
|
|
82
|
+
cp wayfinder_paths/config.example.json config.json
|
|
83
|
+
|
|
84
|
+
# Check status / health
|
|
85
|
+
poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action status --config $(pwd)/config.json
|
|
86
|
+
|
|
87
|
+
# Perform a deposit/update/withdraw cycle
|
|
88
|
+
poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action deposit --main-token-amount 25 --gas-token-amount 0.02 --config $(pwd)/config.json
|
|
89
|
+
poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action update --config $(pwd)/config.json
|
|
90
|
+
poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action withdraw --config $(pwd)/config.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Use the manifest directly if you prefer:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
poetry run python wayfinder_paths/run_strategy.py --manifest wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml --action status --config $(pwd)/config.json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Wallet addresses/labels are auto-resolved from `wallets.json`. Set `NETWORK=testnet` in your config to run the orchestration without touching live HyperEVM endpoints.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"smoke": {
|
|
3
|
+
"deposit": {"main_token_amount": 100.0, "gas_token_amount": 0.001},
|
|
4
|
+
"update": {},
|
|
5
|
+
"status": {},
|
|
6
|
+
"withdraw": {}
|
|
7
|
+
},
|
|
8
|
+
"min_deposit_fail": {
|
|
9
|
+
"deposit": {"main_token_amount": 0.5, "gas_token_amount": 0.0},
|
|
10
|
+
"expect": {"success": false}
|
|
11
|
+
},
|
|
12
|
+
"gas_exceeds_maximum": {
|
|
13
|
+
"deposit": {"main_token_amount": 100.0, "gas_token_amount": 0.2},
|
|
14
|
+
"expect": {"success": false}
|
|
15
|
+
}
|
|
16
|
+
}
|