wayfinder-paths 0.1.7__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 (149) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +399 -0
  2. wayfinder_paths/__init__.py +22 -0
  3. wayfinder_paths/abis/generic/erc20.json +383 -0
  4. wayfinder_paths/adapters/__init__.py +0 -0
  5. wayfinder_paths/adapters/balance_adapter/README.md +94 -0
  6. wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
  7. wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
  8. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  9. wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
  10. wayfinder_paths/adapters/brap_adapter/README.md +249 -0
  11. wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
  12. wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
  13. wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
  15. wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
  16. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
  18. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
  19. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
  20. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
  21. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
  23. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
  24. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
  28. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
  29. wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
  30. wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
  31. wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
  32. wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
  33. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
  34. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
  35. wayfinder_paths/adapters/pool_adapter/README.md +206 -0
  36. wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
  37. wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
  38. wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
  39. wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
  40. wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
  41. wayfinder_paths/adapters/token_adapter/README.md +101 -0
  42. wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
  43. wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
  44. wayfinder_paths/adapters/token_adapter/examples.json +26 -0
  45. wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
  46. wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
  47. wayfinder_paths/config.example.json +22 -0
  48. wayfinder_paths/conftest.py +31 -0
  49. wayfinder_paths/core/__init__.py +18 -0
  50. wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
  51. wayfinder_paths/core/adapters/__init__.py +5 -0
  52. wayfinder_paths/core/adapters/base.py +5 -0
  53. wayfinder_paths/core/adapters/models.py +46 -0
  54. wayfinder_paths/core/analytics/__init__.py +11 -0
  55. wayfinder_paths/core/analytics/bootstrap.py +57 -0
  56. wayfinder_paths/core/analytics/stats.py +48 -0
  57. wayfinder_paths/core/analytics/test_analytics.py +170 -0
  58. wayfinder_paths/core/clients/AuthClient.py +83 -0
  59. wayfinder_paths/core/clients/BRAPClient.py +109 -0
  60. wayfinder_paths/core/clients/ClientManager.py +210 -0
  61. wayfinder_paths/core/clients/HyperlendClient.py +192 -0
  62. wayfinder_paths/core/clients/LedgerClient.py +443 -0
  63. wayfinder_paths/core/clients/PoolClient.py +128 -0
  64. wayfinder_paths/core/clients/SimulationClient.py +192 -0
  65. wayfinder_paths/core/clients/TokenClient.py +89 -0
  66. wayfinder_paths/core/clients/TransactionClient.py +63 -0
  67. wayfinder_paths/core/clients/WalletClient.py +94 -0
  68. wayfinder_paths/core/clients/WayfinderClient.py +269 -0
  69. wayfinder_paths/core/clients/__init__.py +48 -0
  70. wayfinder_paths/core/clients/protocols.py +392 -0
  71. wayfinder_paths/core/clients/sdk_example.py +110 -0
  72. wayfinder_paths/core/config.py +458 -0
  73. wayfinder_paths/core/constants/__init__.py +26 -0
  74. wayfinder_paths/core/constants/base.py +42 -0
  75. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  76. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  77. wayfinder_paths/core/engine/StrategyJob.py +188 -0
  78. wayfinder_paths/core/engine/__init__.py +5 -0
  79. wayfinder_paths/core/engine/manifest.py +97 -0
  80. wayfinder_paths/core/services/__init__.py +0 -0
  81. wayfinder_paths/core/services/base.py +179 -0
  82. wayfinder_paths/core/services/local_evm_txn.py +430 -0
  83. wayfinder_paths/core/services/local_token_txn.py +231 -0
  84. wayfinder_paths/core/services/web3_service.py +45 -0
  85. wayfinder_paths/core/settings.py +61 -0
  86. wayfinder_paths/core/strategies/Strategy.py +280 -0
  87. wayfinder_paths/core/strategies/__init__.py +5 -0
  88. wayfinder_paths/core/strategies/base.py +7 -0
  89. wayfinder_paths/core/strategies/descriptors.py +81 -0
  90. wayfinder_paths/core/utils/__init__.py +1 -0
  91. wayfinder_paths/core/utils/evm_helpers.py +206 -0
  92. wayfinder_paths/core/utils/wallets.py +77 -0
  93. wayfinder_paths/core/wallets/README.md +91 -0
  94. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  95. wayfinder_paths/core/wallets/__init__.py +7 -0
  96. wayfinder_paths/policies/enso.py +17 -0
  97. wayfinder_paths/policies/erc20.py +34 -0
  98. wayfinder_paths/policies/evm.py +21 -0
  99. wayfinder_paths/policies/hyper_evm.py +19 -0
  100. wayfinder_paths/policies/hyperlend.py +12 -0
  101. wayfinder_paths/policies/hyperliquid.py +30 -0
  102. wayfinder_paths/policies/moonwell.py +54 -0
  103. wayfinder_paths/policies/prjx.py +30 -0
  104. wayfinder_paths/policies/util.py +27 -0
  105. wayfinder_paths/run_strategy.py +411 -0
  106. wayfinder_paths/scripts/__init__.py +0 -0
  107. wayfinder_paths/scripts/create_strategy.py +181 -0
  108. wayfinder_paths/scripts/make_wallets.py +169 -0
  109. wayfinder_paths/scripts/run_strategy.py +124 -0
  110. wayfinder_paths/scripts/validate_manifests.py +213 -0
  111. wayfinder_paths/strategies/__init__.py +0 -0
  112. wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
  113. wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
  114. wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
  115. wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
  116. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
  117. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
  118. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
  119. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
  120. wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
  121. wayfinder_paths/strategies/config.py +85 -0
  122. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
  123. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
  124. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  125. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
  126. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
  127. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
  128. wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
  129. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  130. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
  131. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
  132. wayfinder_paths/templates/adapter/README.md +105 -0
  133. wayfinder_paths/templates/adapter/adapter.py +26 -0
  134. wayfinder_paths/templates/adapter/examples.json +8 -0
  135. wayfinder_paths/templates/adapter/manifest.yaml +6 -0
  136. wayfinder_paths/templates/adapter/test_adapter.py +49 -0
  137. wayfinder_paths/templates/strategy/README.md +153 -0
  138. wayfinder_paths/templates/strategy/examples.json +11 -0
  139. wayfinder_paths/templates/strategy/manifest.yaml +8 -0
  140. wayfinder_paths/templates/strategy/strategy.py +57 -0
  141. wayfinder_paths/templates/strategy/test_strategy.py +197 -0
  142. wayfinder_paths/tests/__init__.py +0 -0
  143. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  144. wayfinder_paths/tests/test_test_coverage.py +212 -0
  145. wayfinder_paths/tests/test_utils.py +64 -0
  146. wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
  147. wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
  148. wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
  149. wayfinder_paths-0.1.7.dist-info/WHEEL +4 -0
@@ -0,0 +1,305 @@
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 (
9
+ AssetsView,
10
+ HyperlendClient,
11
+ LendRateHistory,
12
+ MarketEntry,
13
+ StableMarket,
14
+ )
15
+ from wayfinder_paths.core.clients.SimulationClient import SimulationClient
16
+ from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
17
+ from wayfinder_paths.core.constants.hyperlend_abi import (
18
+ POOL_ABI,
19
+ WRAPPED_TOKEN_GATEWAY_ABI,
20
+ )
21
+ from wayfinder_paths.core.services.base import Web3Service
22
+ from wayfinder_paths.core.settings import settings
23
+
24
+ HYPERLEND_DEFAULTS = {
25
+ "pool": "0x00A89d7a5A02160f20150EbEA7a2b5E4879A1A8b",
26
+ "wrapped_token_gateway": "0x49558c794ea2aC8974C9F27886DDfAa951E99171",
27
+ "wrapped_native_underlying": "0x5555555555555555555555555555555555555555",
28
+ }
29
+
30
+
31
+ class HyperlendAdapter(BaseAdapter):
32
+ """Thin HyperLend adapter that only builds tx data and lets the provider send it."""
33
+
34
+ adapter_type = "HYPERLEND"
35
+
36
+ def __init__(
37
+ self,
38
+ config: dict[str, Any],
39
+ web3_service: Web3Service,
40
+ simulation: bool = False,
41
+ ) -> None:
42
+ super().__init__("hyperlend_adapter", config)
43
+ cfg = config or {}
44
+ adapter_cfg = cfg.get("hyperlend_adapter") or {}
45
+
46
+ self.hyperlend_client = HyperlendClient()
47
+ self.simulation = simulation
48
+ self.simulation_client = SimulationClient() if simulation else None
49
+
50
+ self.web3 = web3_service
51
+ self.token_txn_service = web3_service.token_transactions
52
+
53
+ self.strategy_wallet = cfg.get("strategy_wallet") or {}
54
+ self.pool_address = self._checksum(
55
+ adapter_cfg.get("pool_address") or HYPERLEND_DEFAULTS["pool"]
56
+ )
57
+ self.gateway_address = self._checksum(
58
+ adapter_cfg.get("wrapped_token_gateway")
59
+ or HYPERLEND_DEFAULTS["wrapped_token_gateway"]
60
+ )
61
+ self.wrapped_native = self._checksum(
62
+ adapter_cfg.get("wrapped_native_underlying")
63
+ or HYPERLEND_DEFAULTS["wrapped_native_underlying"]
64
+ )
65
+ self.gateway_deposit_takes_pool = adapter_cfg.get(
66
+ "gateway_deposit_takes_pool", True
67
+ )
68
+
69
+ # ------------------------------------------------------------------ #
70
+ # Public API #
71
+ # ------------------------------------------------------------------ #
72
+
73
+ async def get_stable_markets(
74
+ self,
75
+ chain_id: int,
76
+ required_underlying_tokens: float | None = None,
77
+ buffer_bps: int | None = None,
78
+ min_buffer_tokens: float | None = None,
79
+ is_stable_symbol: bool | None = None,
80
+ ) -> tuple[bool, list[StableMarket] | str]:
81
+ try:
82
+ data = await self.hyperlend_client.get_stable_markets(
83
+ chain_id=chain_id,
84
+ required_underlying_tokens=required_underlying_tokens,
85
+ buffer_bps=buffer_bps,
86
+ min_buffer_tokens=min_buffer_tokens,
87
+ is_stable_symbol=is_stable_symbol,
88
+ )
89
+ return True, data
90
+ except Exception as exc:
91
+ return False, str(exc)
92
+
93
+ async def get_assets_view(
94
+ self, chain_id: int, user_address: str
95
+ ) -> tuple[bool, AssetsView | str]:
96
+ try:
97
+ data = await self.hyperlend_client.get_assets_view(
98
+ chain_id=chain_id, user_address=user_address
99
+ )
100
+ return True, data
101
+ except Exception as exc:
102
+ return False, str(exc)
103
+
104
+ async def get_market_entry(
105
+ self, chain_id: int, underlying: str
106
+ ) -> tuple[bool, MarketEntry | str]:
107
+ try:
108
+ data = await self.hyperlend_client.get_market_entry(chain_id, underlying)
109
+ return True, data
110
+ except Exception as exc:
111
+ return False, str(exc)
112
+
113
+ async def get_lend_rate_history(
114
+ self,
115
+ chain_id: int,
116
+ token_address: str,
117
+ lookback_hours: int,
118
+ ) -> tuple[bool, LendRateHistory | str]:
119
+ try:
120
+ data = await self.hyperlend_client.get_lend_rate_history(
121
+ chain_id=chain_id,
122
+ token_address=token_address,
123
+ lookback_hours=lookback_hours,
124
+ )
125
+ return True, data
126
+ except Exception as exc:
127
+ return False, str(exc)
128
+
129
+ async def lend(
130
+ self,
131
+ *,
132
+ underlying_token: str,
133
+ qty: int,
134
+ chain_id: int,
135
+ native: bool = False,
136
+ ) -> tuple[bool, Any]:
137
+ strategy = self._strategy_address()
138
+ qty = int(qty)
139
+ if qty <= 0:
140
+ return False, "qty must be positive"
141
+ chain_id = int(chain_id)
142
+
143
+ if native:
144
+ tx = self._encode_call(
145
+ target=self.gateway_address,
146
+ abi=WRAPPED_TOKEN_GATEWAY_ABI,
147
+ fn_name="depositETH",
148
+ args=[self._gateway_first_arg(underlying_token), strategy, 0],
149
+ from_address=strategy,
150
+ chain_id=chain_id,
151
+ value=qty,
152
+ )
153
+ else:
154
+ token_addr = self._checksum(underlying_token)
155
+ approved = await self._ensure_allowance(
156
+ token_address=token_addr,
157
+ owner=strategy,
158
+ spender=self.pool_address,
159
+ amount=qty,
160
+ chain_id=chain_id,
161
+ )
162
+ if not approved[0]:
163
+ return approved
164
+ tx = self._encode_call(
165
+ target=self.pool_address,
166
+ abi=POOL_ABI,
167
+ fn_name="supply",
168
+ args=[token_addr, qty, strategy, 0],
169
+ from_address=strategy,
170
+ chain_id=chain_id,
171
+ )
172
+ return await self._execute(tx)
173
+
174
+ async def unlend(
175
+ self,
176
+ *,
177
+ underlying_token: str,
178
+ qty: int,
179
+ chain_id: int,
180
+ native: bool = False,
181
+ ) -> tuple[bool, Any]:
182
+ strategy = self._strategy_address()
183
+ qty = int(qty)
184
+ if qty <= 0:
185
+ return False, "qty must be positive"
186
+ chain_id = int(chain_id)
187
+
188
+ if native:
189
+ tx = self._encode_call(
190
+ target=self.gateway_address,
191
+ abi=WRAPPED_TOKEN_GATEWAY_ABI,
192
+ fn_name="withdrawETH",
193
+ args=[self._gateway_first_arg(underlying_token), qty, strategy],
194
+ from_address=strategy,
195
+ chain_id=chain_id,
196
+ )
197
+ else:
198
+ token_addr = self._checksum(underlying_token)
199
+ tx = self._encode_call(
200
+ target=self.pool_address,
201
+ abi=POOL_ABI,
202
+ fn_name="withdraw",
203
+ args=[token_addr, qty, strategy],
204
+ from_address=strategy,
205
+ chain_id=chain_id,
206
+ )
207
+ return await self._execute(tx)
208
+
209
+ # ------------------------------------------------------------------ #
210
+ # Helpers #
211
+ # ------------------------------------------------------------------ #
212
+
213
+ async def _ensure_allowance(
214
+ self,
215
+ *,
216
+ token_address: str,
217
+ owner: str,
218
+ spender: str,
219
+ amount: int,
220
+ chain_id: int,
221
+ ) -> tuple[bool, Any]:
222
+ chain = {"id": chain_id}
223
+ allowance = await self.token_txn_service.read_erc20_allowance(
224
+ chain, token_address, owner, spender
225
+ )
226
+ if allowance.get("allowance", 0) >= amount:
227
+ return True, {}
228
+ build_success, approve_tx = self.token_txn_service.build_erc20_approve(
229
+ chain_id=chain_id,
230
+ token_address=token_address,
231
+ from_address=owner,
232
+ spender=spender,
233
+ amount=amount,
234
+ )
235
+ if not build_success:
236
+ return False, approve_tx
237
+ return await self._broadcast_transaction(approve_tx)
238
+
239
+ async def _execute(self, tx: dict[str, Any]) -> tuple[bool, Any]:
240
+ if self.simulation:
241
+ return True, {"simulation": tx}
242
+ return await self.web3.broadcast_transaction(
243
+ tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
244
+ )
245
+
246
+ async def _broadcast_transaction(self, tx: dict[str, Any]) -> tuple[bool, Any]:
247
+ if getattr(settings, "DRY_RUN", False):
248
+ return True, {"dry_run": True, "transaction": tx}
249
+ return await self.web3.evm_transactions.broadcast_transaction(
250
+ tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
251
+ )
252
+
253
+ def _encode_call(
254
+ self,
255
+ *,
256
+ target: str,
257
+ abi: list[dict[str, Any]],
258
+ fn_name: str,
259
+ args: list[Any],
260
+ from_address: str,
261
+ chain_id: int,
262
+ value: int = 0,
263
+ ) -> dict[str, Any]:
264
+ """Encode calldata without touching network."""
265
+ web3 = self.web3.get_web3(chain_id)
266
+ contract = web3.eth.contract(address=target, abi=abi)
267
+ try:
268
+ data = getattr(contract.functions, fn_name)(*args).build_transaction(
269
+ {"from": from_address}
270
+ )["data"]
271
+ except ValueError as exc:
272
+ raise ValueError(f"Failed to encode {fn_name}: {exc}") from exc
273
+
274
+ tx: dict[str, Any] = {
275
+ "chainId": int(chain_id),
276
+ "from": to_checksum_address(from_address),
277
+ "to": to_checksum_address(target),
278
+ "data": data,
279
+ "value": int(value),
280
+ }
281
+ return tx
282
+
283
+ def _strategy_address(self) -> str:
284
+ addr = None
285
+ if isinstance(self.strategy_wallet, dict):
286
+ addr = self.strategy_wallet.get("address") or (
287
+ (self.strategy_wallet.get("evm") or {}).get("address")
288
+ )
289
+ elif isinstance(self.strategy_wallet, str):
290
+ addr = self.strategy_wallet
291
+ if not addr:
292
+ raise ValueError(
293
+ "strategy_wallet address is required for HyperLend operations"
294
+ )
295
+ return to_checksum_address(addr)
296
+
297
+ def _gateway_first_arg(self, underlying_token: str) -> str:
298
+ if self.gateway_deposit_takes_pool:
299
+ return self.pool_address
300
+ return self._checksum(underlying_token) or self.wrapped_native
301
+
302
+ def _checksum(self, address: str | None) -> str:
303
+ if not address:
304
+ raise ValueError("Missing required contract address in HyperLend config")
305
+ return to_checksum_address(address)
@@ -0,0 +1,10 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "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,274 @@
1
+ from types import SimpleNamespace
2
+ from unittest.mock import AsyncMock
3
+
4
+ import pytest
5
+
6
+ from wayfinder_paths.adapters.hyperlend_adapter.adapter import HyperlendAdapter
7
+
8
+
9
+ class TestHyperlendAdapter:
10
+ """Test cases for HyperlendAdapter"""
11
+
12
+ @pytest.fixture
13
+ def mock_hyperlend_client(self):
14
+ """Mock HyperlendClient for testing"""
15
+ mock_client = AsyncMock()
16
+ return mock_client
17
+
18
+ @pytest.fixture
19
+ def mock_web3_service(self):
20
+ """Minimal Web3Service stub for adapter construction."""
21
+ return SimpleNamespace(token_transactions=SimpleNamespace())
22
+
23
+ @pytest.fixture
24
+ def adapter(self, mock_hyperlend_client, mock_web3_service):
25
+ """Create a HyperlendAdapter instance with mocked client for testing"""
26
+ adapter = HyperlendAdapter(
27
+ config={},
28
+ web3_service=mock_web3_service,
29
+ )
30
+ adapter.hyperlend_client = mock_hyperlend_client
31
+ return adapter
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_get_stable_markets_success(self, adapter, mock_hyperlend_client):
35
+ """Test successful stable markets retrieval"""
36
+ mock_response = {
37
+ "markets": [
38
+ {
39
+ "chain_id": 999,
40
+ "underlying_token": "0x1234...",
41
+ "symbol": "USDT",
42
+ "apy": 0.05,
43
+ "available_liquidity": 1000000,
44
+ "buffer_bps": 100,
45
+ "min_buffer_tokens": 100.0,
46
+ },
47
+ {
48
+ "chain_id": 999,
49
+ "underlying_token": "0x5678...",
50
+ "symbol": "USDC",
51
+ "apy": 0.04,
52
+ "available_liquidity": 2000000,
53
+ "buffer_bps": 100,
54
+ "min_buffer_tokens": 100.0,
55
+ },
56
+ ]
57
+ }
58
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
59
+
60
+ success, data = await adapter.get_stable_markets(
61
+ chain_id=999,
62
+ required_underlying_tokens=1000.0,
63
+ buffer_bps=100,
64
+ min_buffer_tokens=100.0,
65
+ )
66
+
67
+ assert success is True
68
+ assert data == mock_response
69
+ mock_hyperlend_client.get_stable_markets.assert_called_once_with(
70
+ chain_id=999,
71
+ required_underlying_tokens=1000.0,
72
+ buffer_bps=100,
73
+ min_buffer_tokens=100.0,
74
+ is_stable_symbol=None,
75
+ )
76
+
77
+ @pytest.mark.asyncio
78
+ async def test_get_stable_markets_minimal_params(
79
+ self, adapter, mock_hyperlend_client
80
+ ):
81
+ """Test stable markets retrieval with only required chain_id"""
82
+ mock_response = {
83
+ "markets": [
84
+ {
85
+ "chain_id": 999,
86
+ "underlying_token": "0x1234...",
87
+ "symbol": "USDT",
88
+ "apy": 0.05,
89
+ }
90
+ ]
91
+ }
92
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
93
+
94
+ success, data = await adapter.get_stable_markets(chain_id=999)
95
+
96
+ assert success is True
97
+ assert data == mock_response
98
+ mock_hyperlend_client.get_stable_markets.assert_called_once_with(
99
+ chain_id=999,
100
+ required_underlying_tokens=None,
101
+ buffer_bps=None,
102
+ min_buffer_tokens=None,
103
+ is_stable_symbol=None,
104
+ )
105
+
106
+ @pytest.mark.asyncio
107
+ async def test_get_stable_markets_partial_params(
108
+ self, adapter, mock_hyperlend_client
109
+ ):
110
+ """Test stable markets retrieval with partial optional parameters"""
111
+ mock_response = {"markets": []}
112
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
113
+
114
+ success, data = await adapter.get_stable_markets(
115
+ chain_id=999, required_underlying_tokens=500.0
116
+ )
117
+
118
+ assert success is True
119
+ assert data == mock_response
120
+ mock_hyperlend_client.get_stable_markets.assert_called_once_with(
121
+ chain_id=999,
122
+ required_underlying_tokens=500.0,
123
+ buffer_bps=None,
124
+ min_buffer_tokens=None,
125
+ is_stable_symbol=None,
126
+ )
127
+
128
+ @pytest.mark.asyncio
129
+ async def test_get_stable_markets_failure(self, adapter, mock_hyperlend_client):
130
+ """Test stable markets retrieval failure"""
131
+ mock_hyperlend_client.get_stable_markets = AsyncMock(
132
+ side_effect=Exception("API Error: Connection timeout")
133
+ )
134
+
135
+ success, data = await adapter.get_stable_markets(chain_id=999)
136
+
137
+ assert success is False
138
+ assert "API Error: Connection timeout" in data
139
+
140
+ @pytest.mark.asyncio
141
+ async def test_get_stable_markets_http_error(self, adapter, mock_hyperlend_client):
142
+ """Test stable markets retrieval with HTTP error"""
143
+ mock_hyperlend_client.get_stable_markets = AsyncMock(
144
+ side_effect=Exception("HTTP 404 Not Found")
145
+ )
146
+
147
+ success, data = await adapter.get_stable_markets(chain_id=999)
148
+
149
+ assert success is False
150
+ assert "404" in data or "Not Found" in data
151
+
152
+ @pytest.mark.asyncio
153
+ async def test_get_stable_markets_empty_response(
154
+ self, adapter, mock_hyperlend_client
155
+ ):
156
+ """Test stable markets retrieval with empty response"""
157
+ mock_response = {"markets": []}
158
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
159
+
160
+ success, data = await adapter.get_stable_markets(chain_id=999)
161
+
162
+ assert success is True
163
+ assert data == mock_response
164
+ assert len(data.get("markets", [])) == 0
165
+
166
+ def test_adapter_type(self, adapter):
167
+ """Test adapter has adapter_type"""
168
+ assert adapter.adapter_type == "HYPERLEND"
169
+
170
+ @pytest.mark.asyncio
171
+ async def test_health_check(self, adapter):
172
+ """Test adapter health check"""
173
+ health = await adapter.health_check()
174
+ assert isinstance(health, dict)
175
+ assert health.get("status") in {"healthy", "unhealthy", "error"}
176
+ assert health.get("adapter") == "HYPERLEND"
177
+
178
+ @pytest.mark.asyncio
179
+ async def test_connect(self, adapter):
180
+ """Test adapter connection"""
181
+ ok = await adapter.connect()
182
+ assert isinstance(ok, bool)
183
+ assert ok is True
184
+
185
+ @pytest.mark.asyncio
186
+ async def test_get_stable_markets_with_is_stable_symbol(
187
+ self, adapter, mock_hyperlend_client
188
+ ):
189
+ """Test stable markets retrieval with is_stable_symbol parameter"""
190
+ mock_response = {
191
+ "markets": [
192
+ {
193
+ "chain_id": 999,
194
+ "underlying_token": "0x1234...",
195
+ "symbol": "USDT",
196
+ "apy": 0.05,
197
+ }
198
+ ]
199
+ }
200
+ mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
201
+
202
+ success, data = await adapter.get_stable_markets(
203
+ chain_id=999, is_stable_symbol=True
204
+ )
205
+
206
+ assert success is True
207
+ assert data == mock_response
208
+ mock_hyperlend_client.get_stable_markets.assert_called_once_with(
209
+ chain_id=999,
210
+ required_underlying_tokens=None,
211
+ buffer_bps=None,
212
+ min_buffer_tokens=None,
213
+ is_stable_symbol=True,
214
+ )
215
+
216
+ @pytest.mark.asyncio
217
+ async def test_get_assets_view_success(self, adapter, mock_hyperlend_client):
218
+ """Test successful assets view retrieval"""
219
+ mock_response = {
220
+ "assets": [
221
+ {
222
+ "token_address": "0x1234...",
223
+ "symbol": "USDT",
224
+ "balance": "1000.0",
225
+ "supplied": "500.0",
226
+ "borrowed": "0.0",
227
+ }
228
+ ],
229
+ "total_value": 1000.0,
230
+ }
231
+ mock_hyperlend_client.get_assets_view = AsyncMock(return_value=mock_response)
232
+
233
+ success, data = await adapter.get_assets_view(
234
+ chain_id=999,
235
+ user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
236
+ )
237
+
238
+ assert success is True
239
+ assert data == mock_response
240
+ mock_hyperlend_client.get_assets_view.assert_called_once_with(
241
+ chain_id=999,
242
+ user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
243
+ )
244
+
245
+ @pytest.mark.asyncio
246
+ async def test_get_assets_view_failure(self, adapter, mock_hyperlend_client):
247
+ """Test assets view retrieval failure"""
248
+ mock_hyperlend_client.get_assets_view = AsyncMock(
249
+ side_effect=Exception("API Error: Invalid address")
250
+ )
251
+
252
+ success, data = await adapter.get_assets_view(
253
+ chain_id=999,
254
+ user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
255
+ )
256
+
257
+ assert success is False
258
+ assert "API Error: Invalid address" in data
259
+
260
+ @pytest.mark.asyncio
261
+ async def test_get_assets_view_empty_response(self, adapter, mock_hyperlend_client):
262
+ """Test assets view retrieval with empty response"""
263
+ mock_response = {"assets": [], "total_value": 0.0}
264
+ mock_hyperlend_client.get_assets_view = AsyncMock(return_value=mock_response)
265
+
266
+ success, data = await adapter.get_assets_view(
267
+ chain_id=999,
268
+ user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
269
+ )
270
+
271
+ assert success is True
272
+ assert data == mock_response
273
+ assert len(data.get("assets", [])) == 0
274
+ assert data.get("total_value") == 0.0
@@ -0,0 +1,18 @@
1
+ from .adapter import (
2
+ ARBITRUM_USDC_ADDRESS,
3
+ HYPERLIQUID_BRIDGE_ADDRESS,
4
+ HyperliquidAdapter,
5
+ )
6
+ from .executor import HyperliquidExecutor, LocalHyperliquidExecutor
7
+ from .paired_filler import FillConfig, FillConfirmCfg, PairedFiller
8
+
9
+ __all__ = [
10
+ "HyperliquidAdapter",
11
+ "HyperliquidExecutor",
12
+ "LocalHyperliquidExecutor",
13
+ "PairedFiller",
14
+ "FillConfig",
15
+ "FillConfirmCfg",
16
+ "HYPERLIQUID_BRIDGE_ADDRESS",
17
+ "ARBITRUM_USDC_ADDRESS",
18
+ ]