wayfinder-paths 0.1.4__py3-none-any.whl → 0.1.5__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 (60) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +14 -14
  2. wayfinder_paths/__init__.py +3 -3
  3. wayfinder_paths/adapters/balance_adapter/README.md +10 -10
  4. wayfinder_paths/adapters/balance_adapter/adapter.py +10 -9
  5. wayfinder_paths/adapters/balance_adapter/examples.json +1 -1
  6. wayfinder_paths/adapters/brap_adapter/README.md +1 -1
  7. wayfinder_paths/adapters/brap_adapter/adapter.py +28 -21
  8. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +33 -26
  9. wayfinder_paths/adapters/ledger_adapter/README.md +26 -39
  10. wayfinder_paths/adapters/ledger_adapter/adapter.py +78 -75
  11. wayfinder_paths/adapters/ledger_adapter/examples.json +10 -4
  12. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +4 -4
  13. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +31 -26
  14. wayfinder_paths/adapters/pool_adapter/README.md +1 -13
  15. wayfinder_paths/adapters/pool_adapter/adapter.py +12 -19
  16. wayfinder_paths/adapters/token_adapter/adapter.py +8 -4
  17. wayfinder_paths/core/__init__.py +3 -3
  18. wayfinder_paths/core/adapters/BaseAdapter.py +20 -3
  19. wayfinder_paths/core/adapters/models.py +41 -0
  20. wayfinder_paths/core/clients/BRAPClient.py +21 -2
  21. wayfinder_paths/core/clients/ClientManager.py +42 -63
  22. wayfinder_paths/core/clients/HyperlendClient.py +46 -5
  23. wayfinder_paths/core/clients/LedgerClient.py +350 -124
  24. wayfinder_paths/core/clients/PoolClient.py +51 -19
  25. wayfinder_paths/core/clients/SimulationClient.py +16 -4
  26. wayfinder_paths/core/clients/TokenClient.py +34 -18
  27. wayfinder_paths/core/clients/TransactionClient.py +18 -2
  28. wayfinder_paths/core/clients/WalletClient.py +35 -4
  29. wayfinder_paths/core/clients/WayfinderClient.py +16 -5
  30. wayfinder_paths/core/clients/protocols.py +69 -62
  31. wayfinder_paths/core/clients/sdk_example.py +0 -5
  32. wayfinder_paths/core/config.py +192 -103
  33. wayfinder_paths/core/constants/base.py +17 -0
  34. wayfinder_paths/core/engine/{VaultJob.py → StrategyJob.py} +25 -19
  35. wayfinder_paths/core/engine/__init__.py +2 -2
  36. wayfinder_paths/core/services/base.py +6 -4
  37. wayfinder_paths/core/services/local_evm_txn.py +3 -2
  38. wayfinder_paths/core/settings.py +2 -2
  39. wayfinder_paths/core/strategies/Strategy.py +123 -37
  40. wayfinder_paths/core/utils/evm_helpers.py +12 -10
  41. wayfinder_paths/core/wallets/README.md +3 -3
  42. wayfinder_paths/core/wallets/WalletManager.py +3 -3
  43. wayfinder_paths/run_strategy.py +26 -24
  44. wayfinder_paths/scripts/make_wallets.py +6 -6
  45. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +6 -6
  46. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +36 -156
  47. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +6 -6
  48. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +11 -11
  49. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +1 -1
  50. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +92 -92
  51. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +6 -6
  52. wayfinder_paths/templates/adapter/README.md +1 -1
  53. wayfinder_paths/templates/adapter/test_adapter.py +1 -1
  54. wayfinder_paths/templates/strategy/README.md +4 -4
  55. wayfinder_paths/templates/strategy/test_strategy.py +7 -7
  56. wayfinder_paths/tests/test_test_coverage.py +5 -5
  57. {wayfinder_paths-0.1.4.dist-info → wayfinder_paths-0.1.5.dist-info}/METADATA +46 -47
  58. {wayfinder_paths-0.1.4.dist-info → wayfinder_paths-0.1.5.dist-info}/RECORD +60 -59
  59. {wayfinder_paths-0.1.4.dist-info → wayfinder_paths-0.1.5.dist-info}/LICENSE +0 -0
  60. {wayfinder_paths-0.1.4.dist-info → wayfinder_paths-0.1.5.dist-info}/WHEEL +0 -0
@@ -1,7 +1,12 @@
1
1
  from typing import Any
2
2
 
3
3
  from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
4
- from wayfinder_paths.core.clients.PoolClient import PoolClient
4
+ from wayfinder_paths.core.clients.PoolClient import (
5
+ LlamaMatch,
6
+ LlamaReport,
7
+ PoolClient,
8
+ PoolList,
9
+ )
5
10
 
6
11
 
7
12
  class PoolAdapter(BaseAdapter):
@@ -27,7 +32,7 @@ class PoolAdapter(BaseAdapter):
27
32
 
28
33
  async def get_pools_by_ids(
29
34
  self, pool_ids: list[str], merge_external: bool | None = None
30
- ) -> tuple[bool, Any]:
35
+ ) -> tuple[bool, PoolList | str]:
31
36
  """
32
37
  Get pool information by pool IDs.
33
38
 
@@ -50,7 +55,7 @@ class PoolAdapter(BaseAdapter):
50
55
 
51
56
  async def get_all_pools(
52
57
  self, merge_external: bool | None = None
53
- ) -> tuple[bool, Any]:
58
+ ) -> tuple[bool, PoolList | str]:
54
59
  """
55
60
  Get all available pools.
56
61
 
@@ -67,21 +72,7 @@ class PoolAdapter(BaseAdapter):
67
72
  self.logger.error(f"Error fetching all pools: {e}")
68
73
  return (False, str(e))
69
74
 
70
- async def get_combined_pool_reports(self) -> tuple[bool, Any]:
71
- """
72
- Get combined pool reports with analytics.
73
-
74
- Returns:
75
- Tuple of (success, data) where data is combined reports or error message
76
- """
77
- try:
78
- data = await self.pool_client.get_combined_pool_reports()
79
- return (True, data)
80
- except Exception as e:
81
- self.logger.error(f"Error fetching combined pool reports: {e}")
82
- return (False, str(e))
83
-
84
- async def get_llama_matches(self) -> tuple[bool, Any]:
75
+ async def get_llama_matches(self) -> tuple[bool, dict[str, LlamaMatch] | str]:
85
76
  """
86
77
  Get Llama protocol matches for pools.
87
78
 
@@ -95,7 +86,9 @@ class PoolAdapter(BaseAdapter):
95
86
  self.logger.error(f"Error fetching Llama matches: {e}")
96
87
  return (False, str(e))
97
88
 
98
- async def get_llama_reports(self, identifiers: list[str]) -> tuple[bool, Any]:
89
+ async def get_llama_reports(
90
+ self, identifiers: list[str]
91
+ ) -> tuple[bool, dict[str, LlamaReport] | str]:
99
92
  """
100
93
  Get Llama reports for specific identifiers.
101
94
 
@@ -1,7 +1,11 @@
1
1
  from typing import Any
2
2
 
3
3
  from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
4
- from wayfinder_paths.core.clients.TokenClient import TokenClient
4
+ from wayfinder_paths.core.clients.TokenClient import (
5
+ GasToken,
6
+ TokenClient,
7
+ TokenDetails,
8
+ )
5
9
 
6
10
 
7
11
  class TokenAdapter(BaseAdapter):
@@ -20,7 +24,7 @@ class TokenAdapter(BaseAdapter):
20
24
  super().__init__("token_adapter", config)
21
25
  self.token_client = token_client or TokenClient()
22
26
 
23
- async def get_token(self, query: str) -> tuple[bool, Any]:
27
+ async def get_token(self, query: str) -> tuple[bool, TokenDetails | str]:
24
28
  """
25
29
  Get token data by address using the token-details endpoint.
26
30
 
@@ -39,7 +43,7 @@ class TokenAdapter(BaseAdapter):
39
43
  self.logger.error(f"Error getting token by query {query}: {e}")
40
44
  return (False, str(e))
41
45
 
42
- async def get_token_price(self, token_id: str) -> tuple[bool, Any]:
46
+ async def get_token_price(self, token_id: str) -> tuple[bool, dict[str, Any] | str]:
43
47
  """
44
48
  Get token price by token ID or address using the token-details endpoint.
45
49
 
@@ -72,7 +76,7 @@ class TokenAdapter(BaseAdapter):
72
76
  self.logger.error(f"Error getting token price for {token_id}: {e}")
73
77
  return (False, str(e))
74
78
 
75
- async def get_gas_token(self, chain_code: str) -> tuple[bool, Any]:
79
+ async def get_gas_token(self, chain_code: str) -> tuple[bool, GasToken | str]:
76
80
  """
77
81
  Get gas token for a given chain code.
78
82
 
@@ -1,7 +1,7 @@
1
- """Wayfinder Vaults Core Engine"""
1
+ """Wayfinder Paths Core Engine"""
2
2
 
3
3
  from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
4
- from wayfinder_paths.core.engine.VaultJob import VaultJob
4
+ from wayfinder_paths.core.engine.StrategyJob import StrategyJob
5
5
  from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
6
6
 
7
7
  __all__ = [
@@ -9,5 +9,5 @@ __all__ = [
9
9
  "StatusDict",
10
10
  "StatusTuple",
11
11
  "BaseAdapter",
12
- "VaultJob",
12
+ "StrategyJob",
13
13
  ]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import ABC
2
4
  from typing import Any
3
5
 
@@ -7,7 +9,7 @@ from loguru import logger
7
9
  class BaseAdapter(ABC):
8
10
  """Base adapter class for exchange/protocol integrations"""
9
11
 
10
- adapter_type: str = None
12
+ adapter_type: str | None = None
11
13
 
12
14
  def __init__(self, name: str, config: dict[str, Any] | None = None):
13
15
  self.name = name
@@ -19,7 +21,22 @@ class BaseAdapter(ABC):
19
21
  return True
20
22
 
21
23
  async def get_balance(self, asset: str) -> dict[str, Any]:
22
- """Optional: provide balance. Default is not implemented."""
24
+ """
25
+ Get balance for an asset.
26
+ Optional method that can be overridden by subclasses.
27
+
28
+ Args:
29
+ asset: Asset identifier (token address, token ID, etc.).
30
+
31
+ Returns:
32
+ Dictionary containing balance information.
33
+
34
+ Raises:
35
+ ValueError: If asset is empty or invalid.
36
+ NotImplementedError: If this adapter does not support balance queries.
37
+ """
38
+ if not asset or not isinstance(asset, str) or not asset.strip():
39
+ raise ValueError("asset must be a non-empty string")
23
40
  raise NotImplementedError(
24
41
  f"get_balance not supported by {self.__class__.__name__}"
25
42
  )
@@ -43,6 +60,6 @@ class BaseAdapter(ABC):
43
60
  "adapter": self.adapter_type or self.__class__.__name__,
44
61
  }
45
62
 
46
- async def close(self):
63
+ async def close(self) -> None:
47
64
  """Clean up resources"""
48
65
  pass
@@ -0,0 +1,41 @@
1
+ """Pydantic models for ledger operations."""
2
+
3
+ from typing import Annotated, Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SWAP(BaseModel):
9
+ """Swap operation."""
10
+
11
+ type: Literal["SWAP"] = "SWAP"
12
+ from_token_id: str
13
+ to_token_id: str
14
+ from_amount: str
15
+ to_amount: str
16
+ from_amount_usd: float
17
+ to_amount_usd: float
18
+ transaction_hash: str | None = None
19
+ transaction_status: str | None = None
20
+ transaction_receipt: dict[str, Any] | None = None
21
+
22
+
23
+ class LEND(BaseModel):
24
+ type: Literal["LEND"] = "LEND"
25
+ contract: str
26
+ amount: int
27
+
28
+
29
+ class UNLEND(BaseModel):
30
+ type: Literal["UNLEND"] = "UNLEND"
31
+ contract: str
32
+ amount: int
33
+
34
+
35
+ # Type alias for operation types (currently only SWAP is used)
36
+ # Add more operation types here as needed
37
+ Operation = SWAP | LEND | UNLEND
38
+
39
+
40
+ class STRAT_OP(BaseModel):
41
+ op_data: Annotated[Operation, Field(discriminator="type")]
@@ -3,8 +3,10 @@ BRAP (Bridge/Router/Adapter Protocol) Client
3
3
  Provides access to quote operations via the public quote endpoint.
4
4
  """
5
5
 
6
+ from __future__ import annotations
7
+
6
8
  import time
7
- from typing import Any
9
+ from typing import Any, NotRequired, Required, TypedDict
8
10
 
9
11
  from loguru import logger
10
12
 
@@ -13,6 +15,23 @@ from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
13
15
  from wayfinder_paths.core.settings import settings
14
16
 
15
17
 
18
+ class BRAPQuote(TypedDict):
19
+ """BRAP quote response structure"""
20
+
21
+ from_token_address: Required[str]
22
+ to_token_address: Required[str]
23
+ from_chain_id: Required[int]
24
+ to_chain_id: Required[int]
25
+ from_address: Required[str]
26
+ to_address: Required[str]
27
+ amount1: Required[str]
28
+ amount2: NotRequired[str]
29
+ routes: NotRequired[list[dict[str, Any]]]
30
+ fees: NotRequired[dict[str, Any] | None]
31
+ slippage: NotRequired[float | None]
32
+ wayfinder_fee: NotRequired[float | None]
33
+
34
+
16
35
  class BRAPClient(WayfinderClient):
17
36
  """Client for BRAP quote operations"""
18
37
 
@@ -33,7 +52,7 @@ class BRAPClient(WayfinderClient):
33
52
  amount1: str,
34
53
  slippage: float | None = None,
35
54
  wayfinder_fee: float | None = None,
36
- ) -> dict[str, Any]:
55
+ ) -> BRAPQuote:
37
56
  """
38
57
  Get a quote for a bridge/swap operation.
39
58
 
@@ -28,7 +28,7 @@ from wayfinder_paths.core.clients.WalletClient import WalletClient
28
28
 
29
29
  class ClientManager:
30
30
  """
31
- Manages all API client instances for a vault job.
31
+ Manages all API client instances for a strategy job.
32
32
 
33
33
  Args:
34
34
  clients: Optional dict of pre-instantiated clients to inject directly.
@@ -66,6 +66,33 @@ class ClientManager:
66
66
  self._brap_client: BRAPClientProtocol | None = None
67
67
  self._simulation_client: SimulationClientProtocol | None = None
68
68
 
69
+ def _get_or_create_client(
70
+ self,
71
+ client_attr: str,
72
+ injected_key: str,
73
+ client_class: type[Any],
74
+ ) -> Any:
75
+ """
76
+ Helper method to get or create a client instance.
77
+
78
+ Args:
79
+ client_attr: Name of the private attribute storing the client (e.g., "_token_client").
80
+ injected_key: Key to look up in _injected_clients dict.
81
+ client_class: Client class to instantiate if not injected.
82
+
83
+ Returns:
84
+ Client instance.
85
+ """
86
+ client = getattr(self, client_attr)
87
+ if not client:
88
+ client = self._injected_clients.get(injected_key) or client_class(
89
+ api_key=self._api_key
90
+ )
91
+ setattr(self, client_attr, client)
92
+ if self._access_token and hasattr(client, "set_bearer_token"):
93
+ client.set_bearer_token(self._access_token)
94
+ return client
95
+
69
96
  @property
70
97
  def auth(self) -> AuthClient | None:
71
98
  """Get or create auth client. Returns None if skip_auth=True."""
@@ -80,96 +107,48 @@ class ClientManager:
80
107
  @property
81
108
  def token(self) -> TokenClientProtocol:
82
109
  """Get or create token client"""
83
- if not self._token_client:
84
- self._token_client = self._injected_clients.get("token") or TokenClient(
85
- api_key=self._api_key
86
- )
87
- if self._access_token and hasattr(self._token_client, "set_bearer_token"):
88
- self._token_client.set_bearer_token(self._access_token)
89
- return self._token_client
110
+ return self._get_or_create_client("_token_client", "token", TokenClient)
90
111
 
91
112
  @property
92
113
  def transaction(self) -> TransactionClientProtocol:
93
114
  """Get or create transaction client"""
94
- if not self._transaction_client:
95
- self._transaction_client = self._injected_clients.get(
96
- "transaction"
97
- ) or TransactionClient(api_key=self._api_key)
98
- if self._access_token and hasattr(
99
- self._transaction_client, "set_bearer_token"
100
- ):
101
- self._transaction_client.set_bearer_token(self._access_token)
102
- return self._transaction_client
115
+ return self._get_or_create_client(
116
+ "_transaction_client", "transaction", TransactionClient
117
+ )
103
118
 
104
119
  @property
105
120
  def ledger(self) -> LedgerClientProtocol:
106
121
  """Get or create ledger client"""
107
- if not self._ledger_client:
108
- self._ledger_client = self._injected_clients.get("ledger") or LedgerClient(
109
- api_key=self._api_key
110
- )
111
- if self._access_token and hasattr(self._ledger_client, "set_bearer_token"):
112
- self._ledger_client.set_bearer_token(self._access_token)
113
- return self._ledger_client
122
+ return self._get_or_create_client("_ledger_client", "ledger", LedgerClient)
114
123
 
115
124
  @property
116
125
  def pool(self) -> PoolClientProtocol:
117
126
  """Get or create pool client"""
118
- if not self._pool_client:
119
- self._pool_client = self._injected_clients.get("pool") or PoolClient(
120
- api_key=self._api_key
121
- )
122
- if self._access_token and hasattr(self._pool_client, "set_bearer_token"):
123
- self._pool_client.set_bearer_token(self._access_token)
124
- return self._pool_client
127
+ return self._get_or_create_client("_pool_client", "pool", PoolClient)
125
128
 
126
129
  @property
127
130
  def hyperlend(self) -> HyperlendClientProtocol:
128
131
  """Get or create hyperlend client"""
129
- if not self._hyperlend_client:
130
- self._hyperlend_client = self._injected_clients.get(
131
- "hyperlend"
132
- ) or HyperlendClient(api_key=self._api_key)
133
- if self._access_token and hasattr(
134
- self._hyperlend_client, "set_bearer_token"
135
- ):
136
- self._hyperlend_client.set_bearer_token(self._access_token)
137
- return self._hyperlend_client
132
+ return self._get_or_create_client(
133
+ "_hyperlend_client", "hyperlend", HyperlendClient
134
+ )
138
135
 
139
136
  @property
140
137
  def wallet(self) -> WalletClientProtocol:
141
138
  """Get or create wallet client"""
142
- if not self._wallet_client:
143
- self._wallet_client = self._injected_clients.get("wallet") or WalletClient(
144
- api_key=self._api_key
145
- )
146
- if self._access_token and hasattr(self._wallet_client, "set_bearer_token"):
147
- self._wallet_client.set_bearer_token(self._access_token)
148
- return self._wallet_client
139
+ return self._get_or_create_client("_wallet_client", "wallet", WalletClient)
149
140
 
150
141
  @property
151
142
  def brap(self) -> BRAPClientProtocol:
152
143
  """Get or create BRAP client"""
153
- if not self._brap_client:
154
- self._brap_client = self._injected_clients.get("brap") or BRAPClient(
155
- api_key=self._api_key
156
- )
157
- if self._access_token and hasattr(self._brap_client, "set_bearer_token"):
158
- self._brap_client.set_bearer_token(self._access_token)
159
- return self._brap_client
144
+ return self._get_or_create_client("_brap_client", "brap", BRAPClient)
160
145
 
161
146
  @property
162
147
  def simulation(self) -> SimulationClientProtocol:
163
148
  """Get or create simulation client"""
164
- if not self._simulation_client:
165
- self._simulation_client = self._injected_clients.get(
166
- "simulation"
167
- ) or SimulationClient(api_key=self._api_key)
168
- if self._access_token and hasattr(
169
- self._simulation_client, "set_bearer_token"
170
- ):
171
- self._simulation_client.set_bearer_token(self._access_token)
172
- return self._simulation_client
149
+ return self._get_or_create_client(
150
+ "_simulation_client", "simulation", SimulationClient
151
+ )
173
152
 
174
153
  async def authenticate(
175
154
  self,
@@ -3,12 +3,53 @@ Hyperlend Client
3
3
  Provides access to Hyperlend stable markets data via public endpoints.
4
4
  """
5
5
 
6
- from typing import Any
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, NotRequired, Required, TypedDict
7
9
 
8
10
  from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
9
11
  from wayfinder_paths.core.settings import settings
10
12
 
11
13
 
14
+ class StableMarket(TypedDict):
15
+ """Stable market data structure"""
16
+
17
+ chain_id: Required[int]
18
+ token_address: Required[str]
19
+ symbol: Required[str]
20
+ name: Required[str]
21
+ underlying_tokens: Required[float]
22
+ buffer_bps: Required[int]
23
+ min_buffer_tokens: Required[float]
24
+ is_stable_symbol: Required[bool]
25
+
26
+
27
+ class AssetsView(TypedDict):
28
+ """Assets view response structure"""
29
+
30
+ chain_id: Required[int]
31
+ user_address: Required[str]
32
+ assets: Required[list[dict[str, Any]]]
33
+ total_value: NotRequired[float | None]
34
+
35
+
36
+ class MarketEntry(TypedDict):
37
+ """Market entry data structure"""
38
+
39
+ chain_id: Required[int]
40
+ token_address: Required[str]
41
+ market_data: Required[dict[str, Any]]
42
+
43
+
44
+ class LendRateHistory(TypedDict):
45
+ """Lend rate history response structure"""
46
+
47
+ chain_id: Required[int]
48
+ token_address: Required[str]
49
+ lookback_hours: Required[int]
50
+ rates: Required[list[dict[str, Any]]]
51
+
52
+
12
53
  class HyperlendClient(WayfinderClient):
13
54
  """Client for Hyperlend-related operations"""
14
55
 
@@ -24,7 +65,7 @@ class HyperlendClient(WayfinderClient):
24
65
  buffer_bps: int | None = None,
25
66
  min_buffer_tokens: float | None = None,
26
67
  is_stable_symbol: bool | None = None,
27
- ) -> dict[str, Any]:
68
+ ) -> list[StableMarket]:
28
69
  """
29
70
  Fetch stable markets from Hyperlend.
30
71
 
@@ -62,7 +103,7 @@ class HyperlendClient(WayfinderClient):
62
103
  *,
63
104
  chain_id: int,
64
105
  user_address: str,
65
- ) -> dict[str, Any]:
106
+ ) -> AssetsView:
66
107
  """
67
108
  Fetch assets view for a user address from Hyperlend.
68
109
 
@@ -92,7 +133,7 @@ class HyperlendClient(WayfinderClient):
92
133
  *,
93
134
  chain_id: int,
94
135
  token_address: str,
95
- ) -> dict[str, Any]:
136
+ ) -> MarketEntry:
96
137
  """
97
138
  Fetch market entry from Hyperlend.
98
139
 
@@ -123,7 +164,7 @@ class HyperlendClient(WayfinderClient):
123
164
  chain_id: int,
124
165
  token_address: str,
125
166
  lookback_hours: int,
126
- ) -> dict[str, Any]:
167
+ ) -> LendRateHistory:
127
168
  """
128
169
  Fetch lend rate history from Hyperlend.
129
170