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,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,3 @@
1
+ from .adapter import TokenAdapter
2
+
3
+ __all__ = ["TokenAdapter"]
@@ -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,6 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "vaults.adapters.token_adapter.adapter.TokenAdapter"
3
+ capabilities:
4
+ - "token.read"
5
+ dependencies:
6
+ - "TokenClient"
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "vaults.strategies.hyperlend_stable_yield_strategy.strategy.HyperlendStableYieldStrategy"
3
+ permissions:
4
+ policy: "(wallet.id == 'FORMAT_WALLET_ID')"
5
+ adapters:
6
+ - name: "BALANCE"
7
+ capabilities: ["wallet_read"]