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,298 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from eth_utils import to_checksum_address
6
+
7
+ from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
8
+ from wayfinder_paths.core.clients.HyperlendClient import HyperlendClient
9
+ from wayfinder_paths.core.clients.SimulationClient import SimulationClient
10
+ from wayfinder_paths.core.constants.hyperlend_abi import (
11
+ POOL_ABI,
12
+ WRAPPED_TOKEN_GATEWAY_ABI,
13
+ )
14
+ from wayfinder_paths.core.services.base import Web3Service
15
+ from wayfinder_paths.core.settings import settings
16
+
17
+ HYPERLEND_DEFAULTS = {
18
+ "pool": "0x00A89d7a5A02160f20150EbEA7a2b5E4879A1A8b",
19
+ "wrapped_token_gateway": "0x49558c794ea2aC8974C9F27886DDfAa951E99171",
20
+ "wrapped_native_underlying": "0x5555555555555555555555555555555555555555",
21
+ }
22
+
23
+
24
+ class HyperlendAdapter(BaseAdapter):
25
+ """Thin HyperLend adapter that only builds tx data and lets the provider send it."""
26
+
27
+ adapter_type = "HYPERLEND"
28
+
29
+ def __init__(
30
+ self,
31
+ config: dict[str, Any],
32
+ web3_service: Web3Service,
33
+ simulation: bool = False,
34
+ ) -> None:
35
+ super().__init__("hyperlend_adapter", config)
36
+ cfg = config or {}
37
+ adapter_cfg = cfg.get("hyperlend_adapter") or {}
38
+
39
+ self.hyperlend_client = HyperlendClient()
40
+ self.simulation = simulation
41
+ self.simulation_client = SimulationClient() if simulation else None
42
+
43
+ self.web3 = web3_service
44
+ self.token_txn_service = web3_service.token_transactions
45
+
46
+ self.vault_wallet = cfg.get("vault_wallet") or {}
47
+ self.pool_address = self._checksum(
48
+ adapter_cfg.get("pool_address") or HYPERLEND_DEFAULTS["pool"]
49
+ )
50
+ self.gateway_address = self._checksum(
51
+ adapter_cfg.get("wrapped_token_gateway")
52
+ or HYPERLEND_DEFAULTS["wrapped_token_gateway"]
53
+ )
54
+ self.wrapped_native = self._checksum(
55
+ adapter_cfg.get("wrapped_native_underlying")
56
+ or HYPERLEND_DEFAULTS["wrapped_native_underlying"]
57
+ )
58
+ self.gateway_deposit_takes_pool = adapter_cfg.get(
59
+ "gateway_deposit_takes_pool", True
60
+ )
61
+
62
+ # ------------------------------------------------------------------ #
63
+ # Public API #
64
+ # ------------------------------------------------------------------ #
65
+
66
+ async def get_stable_markets(
67
+ self,
68
+ chain_id: int,
69
+ required_underlying_tokens: float | None = None,
70
+ buffer_bps: int | None = None,
71
+ min_buffer_tokens: float | None = None,
72
+ is_stable_symbol: bool | None = None,
73
+ ) -> tuple[bool, Any]:
74
+ try:
75
+ data = await self.hyperlend_client.get_stable_markets(
76
+ chain_id=chain_id,
77
+ required_underlying_tokens=required_underlying_tokens,
78
+ buffer_bps=buffer_bps,
79
+ min_buffer_tokens=min_buffer_tokens,
80
+ is_stable_symbol=is_stable_symbol,
81
+ )
82
+ return True, data
83
+ except Exception as exc:
84
+ return False, str(exc)
85
+
86
+ async def get_assets_view(
87
+ self, chain_id: int, user_address: str
88
+ ) -> tuple[bool, Any]:
89
+ try:
90
+ data = await self.hyperlend_client.get_assets_view(
91
+ chain_id=chain_id, user_address=user_address
92
+ )
93
+ return True, data
94
+ except Exception as exc:
95
+ return False, str(exc)
96
+
97
+ async def get_market_entry(
98
+ self, chain_id: int, underlying: str
99
+ ) -> tuple[bool, Any]:
100
+ try:
101
+ data = await self.hyperlend_client.get_market_entry(chain_id, underlying)
102
+ return True, data
103
+ except Exception as exc:
104
+ return False, str(exc)
105
+
106
+ async def get_lend_rate_history(
107
+ self,
108
+ chain_id: int,
109
+ token_address: str,
110
+ lookback_hours: int,
111
+ ) -> tuple[bool, Any]:
112
+ try:
113
+ data = await self.hyperlend_client.get_lend_rate_history(
114
+ chain_id=chain_id,
115
+ token_address=token_address,
116
+ lookback_hours=lookback_hours,
117
+ )
118
+ return True, data
119
+ except Exception as exc:
120
+ return False, str(exc)
121
+
122
+ async def lend(
123
+ self,
124
+ *,
125
+ underlying_token: str,
126
+ qty: int,
127
+ chain_id: int,
128
+ native: bool = False,
129
+ ) -> tuple[bool, Any]:
130
+ vault = self._vault_address()
131
+ qty = int(qty)
132
+ if qty <= 0:
133
+ return False, "qty must be positive"
134
+ chain_id = int(chain_id)
135
+
136
+ if native:
137
+ tx = self._encode_call(
138
+ target=self.gateway_address,
139
+ abi=WRAPPED_TOKEN_GATEWAY_ABI,
140
+ fn_name="depositETH",
141
+ args=[self._gateway_first_arg(underlying_token), vault, 0],
142
+ from_address=vault,
143
+ chain_id=chain_id,
144
+ value=qty,
145
+ )
146
+ else:
147
+ token_addr = self._checksum(underlying_token)
148
+ approved = await self._ensure_allowance(
149
+ token_address=token_addr,
150
+ owner=vault,
151
+ spender=self.pool_address,
152
+ amount=qty,
153
+ chain_id=chain_id,
154
+ )
155
+ if not approved[0]:
156
+ return approved
157
+ tx = self._encode_call(
158
+ target=self.pool_address,
159
+ abi=POOL_ABI,
160
+ fn_name="supply",
161
+ args=[token_addr, qty, vault, 0],
162
+ from_address=vault,
163
+ chain_id=chain_id,
164
+ )
165
+ return await self._execute(tx)
166
+
167
+ async def unlend(
168
+ self,
169
+ *,
170
+ underlying_token: str,
171
+ qty: int,
172
+ chain_id: int,
173
+ native: bool = False,
174
+ ) -> tuple[bool, Any]:
175
+ vault = self._vault_address()
176
+ qty = int(qty)
177
+ if qty <= 0:
178
+ return False, "qty must be positive"
179
+ chain_id = int(chain_id)
180
+
181
+ if native:
182
+ tx = self._encode_call(
183
+ target=self.gateway_address,
184
+ abi=WRAPPED_TOKEN_GATEWAY_ABI,
185
+ fn_name="withdrawETH",
186
+ args=[self._gateway_first_arg(underlying_token), qty, vault],
187
+ from_address=vault,
188
+ chain_id=chain_id,
189
+ )
190
+ else:
191
+ token_addr = self._checksum(underlying_token)
192
+ tx = self._encode_call(
193
+ target=self.pool_address,
194
+ abi=POOL_ABI,
195
+ fn_name="withdraw",
196
+ args=[token_addr, qty, vault],
197
+ from_address=vault,
198
+ chain_id=chain_id,
199
+ )
200
+ return await self._execute(tx)
201
+
202
+ # ------------------------------------------------------------------ #
203
+ # Helpers #
204
+ # ------------------------------------------------------------------ #
205
+
206
+ async def _ensure_allowance(
207
+ self,
208
+ *,
209
+ token_address: str,
210
+ owner: str,
211
+ spender: str,
212
+ amount: int,
213
+ chain_id: int,
214
+ ) -> tuple[bool, Any]:
215
+ chain = {"id": chain_id}
216
+ allowance = await self.token_txn_service.read_erc20_allowance(
217
+ chain, token_address, owner, spender
218
+ )
219
+ if allowance.get("allowance", 0) >= amount:
220
+ return True, {}
221
+ build_success, approve_tx = self.token_txn_service.build_erc20_approve(
222
+ chain_id=chain_id,
223
+ token_address=token_address,
224
+ from_address=owner,
225
+ spender=spender,
226
+ amount=amount,
227
+ )
228
+ if not build_success:
229
+ return False, approve_tx
230
+ return await self._broadcast_transaction(approve_tx)
231
+
232
+ async def _execute(self, tx: dict[str, Any]) -> tuple[bool, Any]:
233
+ if self.simulation:
234
+ return True, {"simulation": tx}
235
+ return await self.web3.broadcast_transaction(
236
+ tx, wait_for_receipt=True, timeout=120
237
+ )
238
+
239
+ async def _broadcast_transaction(self, tx: dict[str, Any]) -> tuple[bool, Any]:
240
+ if getattr(settings, "DRY_RUN", False):
241
+ return True, {"dry_run": True, "transaction": tx}
242
+ return await self.web3.evm_transactions.broadcast_transaction(
243
+ tx, wait_for_receipt=True, timeout=120
244
+ )
245
+
246
+ def _encode_call(
247
+ self,
248
+ *,
249
+ target: str,
250
+ abi: list[dict[str, Any]],
251
+ fn_name: str,
252
+ args: list[Any],
253
+ from_address: str,
254
+ chain_id: int,
255
+ value: int = 0,
256
+ ) -> dict[str, Any]:
257
+ """Encode calldata without touching network."""
258
+ web3 = self.web3.get_web3(chain_id)
259
+ contract = web3.eth.contract(address=target, abi=abi)
260
+ try:
261
+ data = getattr(contract.functions, fn_name)(*args).build_transaction(
262
+ {"from": from_address}
263
+ )["data"]
264
+ except ValueError as exc:
265
+ raise ValueError(f"Failed to encode {fn_name}: {exc}") from exc
266
+
267
+ tx: dict[str, Any] = {
268
+ "chainId": int(chain_id),
269
+ "from": to_checksum_address(from_address),
270
+ "to": to_checksum_address(target),
271
+ "data": data,
272
+ "value": int(value),
273
+ }
274
+ return tx
275
+
276
+ def _vault_address(self) -> str:
277
+ addr = None
278
+ if isinstance(self.vault_wallet, dict):
279
+ addr = self.vault_wallet.get("address") or (
280
+ (self.vault_wallet.get("evm") or {}).get("address")
281
+ )
282
+ elif isinstance(self.vault_wallet, str):
283
+ addr = self.vault_wallet
284
+ if not addr:
285
+ raise ValueError(
286
+ "vault_wallet address is required for HyperLend operations"
287
+ )
288
+ return to_checksum_address(addr)
289
+
290
+ def _gateway_first_arg(self, underlying_token: str) -> str:
291
+ if self.gateway_deposit_takes_pool:
292
+ return self.pool_address
293
+ return self._checksum(underlying_token) or self.wrapped_native
294
+
295
+ def _checksum(self, address: str | None) -> str:
296
+ if not address:
297
+ raise ValueError("Missing required contract address in HyperLend config")
298
+ return to_checksum_address(address)
@@ -0,0 +1,10 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "vaults.adapters.hyperlend_adapter.adapter.HyperlendAdapter"
3
+ capabilities:
4
+ - "hyperlend.stable_markets.read"
5
+ - "hyperlend.markets.query"
6
+ - "hyperlend.assets_view.read"
7
+ - "hyperlend.lend"
8
+ dependencies:
9
+ - "HyperlendClient"
10
+
@@ -0,0 +1,267 @@
1
+ from unittest.mock import AsyncMock, patch
2
+
3
+ import pytest
4
+
5
+ from wayfinder_paths.vaults.adapters.hyperlend_adapter.adapter import HyperlendAdapter
6
+
7
+
8
+ class TestHyperlendAdapter:
9
+ """Test cases for HyperlendAdapter"""
10
+
11
+ @pytest.fixture
12
+ def mock_hyperlend_client(self):
13
+ """Mock HyperlendClient for testing"""
14
+ mock_client = AsyncMock()
15
+ return mock_client
16
+
17
+ @pytest.fixture
18
+ def adapter(self, mock_hyperlend_client):
19
+ """Create a HyperlendAdapter instance with mocked client for testing"""
20
+ with patch(
21
+ "wayfinder_paths.vaults.adapters.hyperlend_adapter.adapter.HyperlendClient",
22
+ return_value=mock_hyperlend_client,
23
+ ):
24
+ return HyperlendAdapter()
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_get_stable_markets_success(self, adapter, mock_hyperlend_client):
28
+ """Test successful stable markets retrieval"""
29
+ mock_response = {
30
+ "markets": [
31
+ {
32
+ "chain_id": 999,
33
+ "underlying_token": "0x1234...",
34
+ "symbol": "USDT",
35
+ "apy": 0.05,
36
+ "available_liquidity": 1000000,
37
+ "buffer_bps": 100,
38
+ "min_buffer_tokens": 100.0,
39
+ },
40
+ {
41
+ "chain_id": 999,
42
+ "underlying_token": "0x5678...",
43
+ "symbol": "USDC",
44
+ "apy": 0.04,
45
+ "available_liquidity": 2000000,
46
+ "buffer_bps": 100,
47
+ "min_buffer_tokens": 100.0,
48
+ },
49
+ ]
50
+ }
51
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
52
+
53
+ success, data = await adapter.get_stable_markets(
54
+ chain_id=999,
55
+ required_underlying_tokens=1000.0,
56
+ buffer_bps=100,
57
+ min_buffer_tokens=100.0,
58
+ )
59
+
60
+ assert success is True
61
+ assert data == mock_response
62
+ mock_hyperlend_client.get_stable_markets.assert_called_once_with(
63
+ chain_id=999,
64
+ required_underlying_tokens=1000.0,
65
+ buffer_bps=100,
66
+ min_buffer_tokens=100.0,
67
+ is_stable_symbol=None,
68
+ )
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_get_stable_markets_minimal_params(
72
+ self, adapter, mock_hyperlend_client
73
+ ):
74
+ """Test stable markets retrieval with only required chain_id"""
75
+ mock_response = {
76
+ "markets": [
77
+ {
78
+ "chain_id": 999,
79
+ "underlying_token": "0x1234...",
80
+ "symbol": "USDT",
81
+ "apy": 0.05,
82
+ }
83
+ ]
84
+ }
85
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
86
+
87
+ success, data = await adapter.get_stable_markets(chain_id=999)
88
+
89
+ assert success is True
90
+ assert data == mock_response
91
+ mock_hyperlend_client.get_stable_markets.assert_called_once_with(
92
+ chain_id=999,
93
+ required_underlying_tokens=None,
94
+ buffer_bps=None,
95
+ min_buffer_tokens=None,
96
+ is_stable_symbol=None,
97
+ )
98
+
99
+ @pytest.mark.asyncio
100
+ async def test_get_stable_markets_partial_params(
101
+ self, adapter, mock_hyperlend_client
102
+ ):
103
+ """Test stable markets retrieval with partial optional parameters"""
104
+ mock_response = {"markets": []}
105
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
106
+
107
+ success, data = await adapter.get_stable_markets(
108
+ chain_id=999, required_underlying_tokens=500.0
109
+ )
110
+
111
+ assert success is True
112
+ assert data == mock_response
113
+ mock_hyperlend_client.get_stable_markets.assert_called_once_with(
114
+ chain_id=999,
115
+ required_underlying_tokens=500.0,
116
+ buffer_bps=None,
117
+ min_buffer_tokens=None,
118
+ is_stable_symbol=None,
119
+ )
120
+
121
+ @pytest.mark.asyncio
122
+ async def test_get_stable_markets_failure(self, adapter, mock_hyperlend_client):
123
+ """Test stable markets retrieval failure"""
124
+ mock_hyperlend_client.get_stable_markets = AsyncMock(
125
+ side_effect=Exception("API Error: Connection timeout")
126
+ )
127
+
128
+ success, data = await adapter.get_stable_markets(chain_id=999)
129
+
130
+ assert success is False
131
+ assert "API Error: Connection timeout" in data
132
+
133
+ @pytest.mark.asyncio
134
+ async def test_get_stable_markets_http_error(self, adapter, mock_hyperlend_client):
135
+ """Test stable markets retrieval with HTTP error"""
136
+ mock_hyperlend_client.get_stable_markets = AsyncMock(
137
+ side_effect=Exception("HTTP 404 Not Found")
138
+ )
139
+
140
+ success, data = await adapter.get_stable_markets(chain_id=999)
141
+
142
+ assert success is False
143
+ assert "404" in data or "Not Found" in data
144
+
145
+ @pytest.mark.asyncio
146
+ async def test_get_stable_markets_empty_response(
147
+ self, adapter, mock_hyperlend_client
148
+ ):
149
+ """Test stable markets retrieval with empty response"""
150
+ mock_response = {"markets": []}
151
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
152
+
153
+ success, data = await adapter.get_stable_markets(chain_id=999)
154
+
155
+ assert success is True
156
+ assert data == mock_response
157
+ assert len(data.get("markets", [])) == 0
158
+
159
+ def test_adapter_type(self, adapter):
160
+ """Test adapter has adapter_type"""
161
+ assert adapter.adapter_type == "HYPERLEND"
162
+
163
+ @pytest.mark.asyncio
164
+ async def test_health_check(self, adapter):
165
+ """Test adapter health check"""
166
+ health = await adapter.health_check()
167
+ assert isinstance(health, dict)
168
+ assert health.get("status") in {"healthy", "unhealthy", "error"}
169
+ assert health.get("adapter") == "HYPERLEND"
170
+
171
+ @pytest.mark.asyncio
172
+ async def test_connect(self, adapter):
173
+ """Test adapter connection"""
174
+ ok = await adapter.connect()
175
+ assert isinstance(ok, bool)
176
+ assert ok is True
177
+
178
+ @pytest.mark.asyncio
179
+ async def test_get_stable_markets_with_is_stable_symbol(
180
+ self, adapter, mock_hyperlend_client
181
+ ):
182
+ """Test stable markets retrieval with is_stable_symbol parameter"""
183
+ mock_response = {
184
+ "markets": [
185
+ {
186
+ "chain_id": 999,
187
+ "underlying_token": "0x1234...",
188
+ "symbol": "USDT",
189
+ "apy": 0.05,
190
+ }
191
+ ]
192
+ }
193
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
194
+
195
+ success, data = await adapter.get_stable_markets(
196
+ chain_id=999, is_stable_symbol=True
197
+ )
198
+
199
+ assert success is True
200
+ assert data == mock_response
201
+ mock_hyperlend_client.get_stable_markets.assert_called_once_with(
202
+ chain_id=999,
203
+ required_underlying_tokens=None,
204
+ buffer_bps=None,
205
+ min_buffer_tokens=None,
206
+ is_stable_symbol=True,
207
+ )
208
+
209
+ @pytest.mark.asyncio
210
+ async def test_get_assets_view_success(self, adapter, mock_hyperlend_client):
211
+ """Test successful assets view retrieval"""
212
+ mock_response = {
213
+ "assets": [
214
+ {
215
+ "token_address": "0x1234...",
216
+ "symbol": "USDT",
217
+ "balance": "1000.0",
218
+ "supplied": "500.0",
219
+ "borrowed": "0.0",
220
+ }
221
+ ],
222
+ "total_value": 1000.0,
223
+ }
224
+ mock_hyperlend_client.get_assets_view = AsyncMock(return_value=mock_response)
225
+
226
+ success, data = await adapter.get_assets_view(
227
+ chain_id=999,
228
+ user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
229
+ )
230
+
231
+ assert success is True
232
+ assert data == mock_response
233
+ mock_hyperlend_client.get_assets_view.assert_called_once_with(
234
+ chain_id=999,
235
+ user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
236
+ )
237
+
238
+ @pytest.mark.asyncio
239
+ async def test_get_assets_view_failure(self, adapter, mock_hyperlend_client):
240
+ """Test assets view retrieval failure"""
241
+ mock_hyperlend_client.get_assets_view = AsyncMock(
242
+ side_effect=Exception("API Error: Invalid address")
243
+ )
244
+
245
+ success, data = await adapter.get_assets_view(
246
+ chain_id=999,
247
+ user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
248
+ )
249
+
250
+ assert success is False
251
+ assert "API Error: Invalid address" in data
252
+
253
+ @pytest.mark.asyncio
254
+ async def test_get_assets_view_empty_response(self, adapter, mock_hyperlend_client):
255
+ """Test assets view retrieval with empty response"""
256
+ mock_response = {"assets": [], "total_value": 0.0}
257
+ mock_hyperlend_client.get_assets_view = AsyncMock(return_value=mock_response)
258
+
259
+ success, data = await adapter.get_assets_view(
260
+ chain_id=999,
261
+ user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
262
+ )
263
+
264
+ assert success is True
265
+ assert data == mock_response
266
+ assert len(data.get("assets", [])) == 0
267
+ assert data.get("total_value") == 0.0