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,90 @@
1
+ """
2
+ BRAP (Bridge/Router/Adapter Protocol) Client
3
+ Provides access to quote operations via the public quote endpoint.
4
+ """
5
+
6
+ import time
7
+ from typing import Any
8
+
9
+ from loguru import logger
10
+
11
+ from wayfinder_paths.core.clients.AuthClient import AuthClient
12
+ from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
13
+ from wayfinder_paths.core.settings import settings
14
+
15
+
16
+ class BRAPClient(WayfinderClient):
17
+ """Client for BRAP quote operations"""
18
+
19
+ def __init__(self, api_key: str | None = None):
20
+ super().__init__(api_key=api_key)
21
+ self.api_base_url = f"{settings.WAYFINDER_API_URL}"
22
+ self._auth_client: AuthClient | None = AuthClient(api_key=api_key)
23
+
24
+ async def get_quote(
25
+ self,
26
+ *,
27
+ from_token_address: str,
28
+ to_token_address: str,
29
+ from_chain_id: int,
30
+ to_chain_id: int,
31
+ from_address: str,
32
+ to_address: str,
33
+ amount1: str,
34
+ slippage: float | None = None,
35
+ wayfinder_fee: float | None = None,
36
+ ) -> dict[str, Any]:
37
+ """
38
+ Get a quote for a bridge/swap operation.
39
+
40
+ Args:
41
+ from_token_address: Source token contract address
42
+ to_token_address: Destination token contract address
43
+ from_chain_id: Source chain ID
44
+ to_chain_id: Destination chain ID
45
+ from_address: Source wallet address
46
+ to_address: Destination wallet address
47
+ amount1: Amount to swap (in smallest units)
48
+ slippage: Maximum slippage tolerance (optional)
49
+ wayfinder_fee: Wayfinder fee (optional)
50
+
51
+ Returns:
52
+ Quote data including routes, amounts, fees, etc.
53
+ """
54
+ logger.info(
55
+ f"Getting BRAP quote: {from_token_address} -> {to_token_address} (chain {from_chain_id} -> {to_chain_id})"
56
+ )
57
+ logger.debug(
58
+ f"Quote params: amount={amount1}, slippage={slippage}, wayfinder_fee={wayfinder_fee}"
59
+ )
60
+ start_time = time.time()
61
+
62
+ url = f"{self.api_base_url}/public/quotes/"
63
+
64
+ payload = {
65
+ "from_token_address": from_token_address,
66
+ "to_token_address": to_token_address,
67
+ "from_chain_id": from_chain_id,
68
+ "to_chain_id": to_chain_id,
69
+ "from_address": from_address,
70
+ "to_address": to_address,
71
+ "amount1": amount1,
72
+ }
73
+
74
+ # Only add optional parameters if they're provided
75
+ if slippage is not None:
76
+ payload["slippage"] = slippage
77
+ if wayfinder_fee is not None:
78
+ payload["wayfinder_fee"] = wayfinder_fee
79
+
80
+ try:
81
+ response = await self._request("POST", url, json=payload, headers={})
82
+ response.raise_for_status()
83
+ data = response.json()
84
+ elapsed = time.time() - start_time
85
+ logger.info(f"BRAP quote request completed successfully in {elapsed:.2f}s")
86
+ return data.get("data", data)
87
+ except Exception as e:
88
+ elapsed = time.time() - start_time
89
+ logger.error(f"BRAP quote request failed after {elapsed:.2f}s: {e}")
90
+ raise
@@ -0,0 +1,231 @@
1
+ """
2
+ Client Manager
3
+ Consolidated client management for all API interactions
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from wayfinder_paths.core.clients.AuthClient import AuthClient
9
+ from wayfinder_paths.core.clients.BRAPClient import BRAPClient
10
+ from wayfinder_paths.core.clients.HyperlendClient import HyperlendClient
11
+ from wayfinder_paths.core.clients.LedgerClient import LedgerClient
12
+ from wayfinder_paths.core.clients.PoolClient import PoolClient
13
+ from wayfinder_paths.core.clients.protocols import (
14
+ BRAPClientProtocol,
15
+ HyperlendClientProtocol,
16
+ LedgerClientProtocol,
17
+ PoolClientProtocol,
18
+ SimulationClientProtocol,
19
+ TokenClientProtocol,
20
+ TransactionClientProtocol,
21
+ WalletClientProtocol,
22
+ )
23
+ from wayfinder_paths.core.clients.SimulationClient import SimulationClient
24
+ from wayfinder_paths.core.clients.TokenClient import TokenClient
25
+ from wayfinder_paths.core.clients.TransactionClient import TransactionClient
26
+ from wayfinder_paths.core.clients.WalletClient import WalletClient
27
+
28
+
29
+ class ClientManager:
30
+ """
31
+ Manages all API client instances for a vault job.
32
+
33
+ Args:
34
+ clients: Optional dict of pre-instantiated clients to inject directly.
35
+ Keys: 'token', 'hyperlend', 'ledger', 'wallet', 'transaction', 'pool', 'brap', 'simulation'.
36
+ If not provided, defaults to HTTP-based clients.
37
+ skip_auth: If True, skips authentication (for SDK usage).
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ clients: dict[str, Any] | None = None,
43
+ skip_auth: bool = False,
44
+ api_key: str | None = None,
45
+ ):
46
+ """
47
+ Initialize ClientManager.
48
+
49
+ Args:
50
+ clients: Optional dict of pre-instantiated clients to inject directly.
51
+ skip_auth: If True, skips authentication (for SDK usage).
52
+ api_key: Optional API key for service account authentication.
53
+ """
54
+ self._injected_clients = clients or {}
55
+ self._skip_auth = skip_auth
56
+ self._api_key = api_key
57
+ self._access_token: str | None = None
58
+
59
+ self._auth_client: AuthClient | None = None
60
+ self._token_client: TokenClientProtocol | None = None
61
+ self._wallet_client: WalletClientProtocol | None = None
62
+ self._transaction_client: TransactionClientProtocol | None = None
63
+ self._ledger_client: LedgerClientProtocol | None = None
64
+ self._pool_client: PoolClientProtocol | None = None
65
+ self._hyperlend_client: HyperlendClientProtocol | None = None
66
+ self._brap_client: BRAPClientProtocol | None = None
67
+ self._simulation_client: SimulationClientProtocol | None = None
68
+
69
+ @property
70
+ def auth(self) -> AuthClient | None:
71
+ """Get or create auth client. Returns None if skip_auth=True."""
72
+ if self._skip_auth:
73
+ return None
74
+ if not self._auth_client:
75
+ self._auth_client = AuthClient(api_key=self._api_key)
76
+ if self._access_token:
77
+ self._auth_client.set_bearer_token(self._access_token)
78
+ return self._auth_client
79
+
80
+ @property
81
+ def token(self) -> TokenClientProtocol:
82
+ """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
90
+
91
+ @property
92
+ def transaction(self) -> TransactionClientProtocol:
93
+ """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
103
+
104
+ @property
105
+ def ledger(self) -> LedgerClientProtocol:
106
+ """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
114
+
115
+ @property
116
+ def pool(self) -> PoolClientProtocol:
117
+ """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
125
+
126
+ @property
127
+ def hyperlend(self) -> HyperlendClientProtocol:
128
+ """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
138
+
139
+ @property
140
+ def wallet(self) -> WalletClientProtocol:
141
+ """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
149
+
150
+ @property
151
+ def brap(self) -> BRAPClientProtocol:
152
+ """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
160
+
161
+ @property
162
+ def simulation(self) -> SimulationClientProtocol:
163
+ """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
173
+
174
+ async def authenticate(
175
+ self,
176
+ username: str | None = None,
177
+ password: str | None = None,
178
+ *,
179
+ refresh_token: str | None = None,
180
+ ) -> dict[str, Any]:
181
+ """Authenticate with the API. Raises ValueError if skip_auth=True."""
182
+ if self._skip_auth:
183
+ raise ValueError(
184
+ "Authentication is disabled in SDK mode. SDK users handle their own authentication."
185
+ )
186
+ auth_client = self.auth
187
+ if auth_client is None:
188
+ raise ValueError("Auth client is not available")
189
+ data = await auth_client.authenticate(
190
+ username, password, refresh_token=refresh_token
191
+ )
192
+ access = data.get("access") or data.get("access_token")
193
+ if access:
194
+ self.set_access_token(access)
195
+ return data
196
+
197
+ def set_access_token(self, access_token: str) -> None:
198
+ """Set and propagate access token to all initialized clients."""
199
+ self._access_token = access_token
200
+ if self._auth_client:
201
+ self._auth_client.set_bearer_token(access_token)
202
+ if self._token_client and hasattr(self._token_client, "set_bearer_token"):
203
+ self._token_client.set_bearer_token(access_token)
204
+ if self._transaction_client and hasattr(
205
+ self._transaction_client, "set_bearer_token"
206
+ ):
207
+ self._transaction_client.set_bearer_token(access_token)
208
+ if self._ledger_client and hasattr(self._ledger_client, "set_bearer_token"):
209
+ self._ledger_client.set_bearer_token(access_token)
210
+ if self._pool_client and hasattr(self._pool_client, "set_bearer_token"):
211
+ self._pool_client.set_bearer_token(access_token)
212
+ if self._hyperlend_client and hasattr(
213
+ self._hyperlend_client, "set_bearer_token"
214
+ ):
215
+ self._hyperlend_client.set_bearer_token(access_token)
216
+ if self._wallet_client and hasattr(self._wallet_client, "set_bearer_token"):
217
+ self._wallet_client.set_bearer_token(access_token)
218
+
219
+ def get_all_clients(self) -> dict[str, Any]:
220
+ """Get all initialized clients for direct access"""
221
+ return {
222
+ "auth": self._auth_client,
223
+ "token": self._token_client,
224
+ "transaction": self._transaction_client,
225
+ "ledger": self._ledger_client,
226
+ "pool": self._pool_client,
227
+ "wallet": self._wallet_client,
228
+ "hyperlend": self._hyperlend_client,
229
+ "brap": self._brap_client,
230
+ "simulation": self._simulation_client,
231
+ }
@@ -0,0 +1,151 @@
1
+ """
2
+ Hyperlend Client
3
+ Provides access to Hyperlend stable markets data via public endpoints.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
9
+ from wayfinder_paths.core.settings import settings
10
+
11
+
12
+ class HyperlendClient(WayfinderClient):
13
+ """Client for Hyperlend-related operations"""
14
+
15
+ def __init__(self, api_key: str | None = None):
16
+ super().__init__(api_key=api_key)
17
+ self.api_base_url = f"{settings.WAYFINDER_API_URL}"
18
+
19
+ async def get_stable_markets(
20
+ self,
21
+ *,
22
+ chain_id: int,
23
+ required_underlying_tokens: float | None = None,
24
+ buffer_bps: int | None = None,
25
+ min_buffer_tokens: float | None = None,
26
+ is_stable_symbol: bool | None = None,
27
+ ) -> dict[str, Any]:
28
+ """
29
+ Fetch stable markets from Hyperlend.
30
+
31
+ Args:
32
+ chain_id: Chain ID to query markets for
33
+ required_underlying_tokens: Required underlying tokens amount
34
+ buffer_bps: Buffer in basis points
35
+ min_buffer_tokens: Minimum buffer in tokens
36
+ is_stable_symbol: Filter by stable symbol (optional)
37
+
38
+ Example:
39
+ GET /api/v1/public/hyperlend/stable-markets/?chain_id=999&required_underlying_tokens=1000.0&buffer_bps=100&min_buffer_tokens=100.0&is_stable_symbol=true
40
+
41
+ Returns:
42
+ Dictionary containing stable markets data
43
+ """
44
+ url = f"{self.api_base_url}/public/hyperlend/stable-markets/"
45
+ params: dict[str, Any] = {"chain_id": chain_id}
46
+ if required_underlying_tokens is not None:
47
+ params["required_underlying_tokens"] = required_underlying_tokens
48
+ if buffer_bps is not None:
49
+ params["buffer_bps"] = buffer_bps
50
+ if min_buffer_tokens is not None:
51
+ params["min_buffer_tokens"] = min_buffer_tokens
52
+ if is_stable_symbol is not None:
53
+ params["is_stable_symbol"] = "true" if is_stable_symbol else "false"
54
+
55
+ response = await self._authed_request("GET", url, params=params, headers={})
56
+ response.raise_for_status()
57
+ data = response.json()
58
+ return data.get("data", data)
59
+
60
+ async def get_assets_view(
61
+ self,
62
+ *,
63
+ chain_id: int,
64
+ user_address: str,
65
+ ) -> dict[str, Any]:
66
+ """
67
+ Fetch assets view for a user address from Hyperlend.
68
+
69
+ Args:
70
+ chain_id: Chain ID to query assets for
71
+ user_address: User wallet address to query assets for
72
+
73
+ Example:
74
+ GET /api/v1/public/hyperlend/assets-view/?chain_id=999&user_address=0x0c737cB5934afCb5B01965141F865F795B324080
75
+
76
+ Returns:
77
+ Dictionary containing assets view data
78
+ """
79
+ url = f"{self.api_base_url}/public/hyperlend/assets-view/"
80
+ params: dict[str, Any] = {
81
+ "chain_id": chain_id,
82
+ "user_address": user_address,
83
+ }
84
+
85
+ response = await self._authed_request("GET", url, params=params, headers={})
86
+ response.raise_for_status()
87
+ data = response.json()
88
+ return data.get("data", data)
89
+
90
+ async def get_market_entry(
91
+ self,
92
+ *,
93
+ chain_id: int,
94
+ token_address: str,
95
+ ) -> dict[str, Any]:
96
+ """
97
+ Fetch market entry from Hyperlend.
98
+
99
+ Args:
100
+ chain_id: Chain ID to query market for
101
+ token_address: Token address to query market for
102
+
103
+ Example:
104
+ GET /api/v1/public/hyperlend/market-entry/?chain_id=999&token_address=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
105
+
106
+ Returns:
107
+ Dictionary containing market entry data
108
+ """
109
+ url = f"{self.api_base_url}/public/hyperlend/market-entry/"
110
+ params: dict[str, Any] = {
111
+ "chain_id": chain_id,
112
+ "token_address": token_address,
113
+ }
114
+
115
+ response = await self._authed_request("GET", url, params=params, headers={})
116
+ response.raise_for_status()
117
+ data = response.json()
118
+ return data.get("data", data)
119
+
120
+ async def get_lend_rate_history(
121
+ self,
122
+ *,
123
+ chain_id: int,
124
+ token_address: str,
125
+ lookback_hours: int,
126
+ ) -> dict[str, Any]:
127
+ """
128
+ Fetch lend rate history from Hyperlend.
129
+
130
+ Args:
131
+ chain_id: Chain ID to query rate history for
132
+ token_address: Token address to query rate history for
133
+ lookback_hours: Number of hours to look back for rate history
134
+
135
+ Example:
136
+ GET /api/v1/public/hyperlend/lend-rate-history/?chain_id=999&token_address=0x5555555555555555555555555555555555555555&lookback_hours=24
137
+
138
+ Returns:
139
+ Dictionary containing lend rate history data
140
+ """
141
+ url = f"{self.api_base_url}/public/hyperlend/lend-rate-history/"
142
+ params: dict[str, Any] = {
143
+ "chain_id": chain_id,
144
+ "token_address": token_address,
145
+ "lookback_hours": lookback_hours,
146
+ }
147
+
148
+ response = await self._authed_request("GET", url, params=params, headers={})
149
+ response.raise_for_status()
150
+ data = response.json()
151
+ return data.get("data", data)
@@ -0,0 +1,222 @@
1
+ from typing import Any
2
+
3
+ from wayfinder_paths.core.clients.AuthClient import AuthClient
4
+ from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
5
+ from wayfinder_paths.core.settings import settings
6
+
7
+
8
+ class LedgerClient(WayfinderClient):
9
+ """
10
+ Client for vault transaction history and bookkeeping operations.
11
+
12
+ Supports:
13
+ - GET vault transactions
14
+ - GET vault net deposit
15
+ - GET vault last rotation time
16
+ - POST add deposit
17
+ - POST add withdraw
18
+ - POST add operation
19
+ - POST add cashflow
20
+ """
21
+
22
+ def __init__(self, api_key: str | None = None) -> None:
23
+ super().__init__(api_key=api_key)
24
+ self.api_base_url = f"{settings.WAYFINDER_API_URL}"
25
+ self._auth_client: AuthClient | None = AuthClient(api_key=api_key)
26
+
27
+ # ===================== Read Endpoints =====================
28
+
29
+ async def get_vault_transactions(
30
+ self,
31
+ *,
32
+ wallet_address: str,
33
+ limit: int = 50,
34
+ offset: int = 0,
35
+ ) -> dict[str, Any]:
36
+ """
37
+ Fetch a paginated list of transactions for a given vault wallet and address.
38
+
39
+ GET /api/v1/public/vaults/transactions/?wallet_address=...&limit=...&offset=...
40
+ """
41
+ url = f"{self.api_base_url}/public/vaults/transactions/"
42
+ params = {
43
+ "wallet_address": wallet_address,
44
+ "limit": str(limit),
45
+ "offset": str(offset),
46
+ }
47
+ response = await self._authed_request("GET", url, params=params)
48
+ data = response.json()
49
+ return data.get("data", data)
50
+
51
+ async def get_vault_net_deposit(self, *, wallet_address: str) -> dict[str, Any]:
52
+ """
53
+ Fetch the net deposit (deposits - withdrawals) for a vault and address.
54
+
55
+ GET /api/v1/public/vaults/net-deposit/?wallet_address=...
56
+ """
57
+ url = f"{self.api_base_url}/public/vaults/net-deposit/"
58
+ params = {
59
+ "wallet_address": wallet_address,
60
+ }
61
+ response = await self._authed_request("GET", url, params=params)
62
+ data = response.json()
63
+ return data.get("data", data)
64
+
65
+ async def get_vault_latest_transactions(
66
+ self, *, wallet_address: str
67
+ ) -> dict[str, Any]:
68
+ """
69
+ Fetch the last rotation time for a vault and address.
70
+
71
+ GET /api/v1/public/vaults/last-rotation-time/?wallet_address=...
72
+ """
73
+ url = f"{self.api_base_url}/public/vaults/latest-transactions/"
74
+ params = {
75
+ "wallet_address": wallet_address,
76
+ }
77
+ response = await self._authed_request("GET", url, params=params)
78
+ data = response.json()
79
+ return data.get("data", data)
80
+
81
+ # ===================== Write Endpoints =====================
82
+
83
+ async def add_vault_deposit(
84
+ self,
85
+ *,
86
+ wallet_address: str,
87
+ chain_id: int,
88
+ token_address: str,
89
+ token_amount: str | float,
90
+ usd_value: str | float,
91
+ data: dict[str, Any] | None = None,
92
+ strategy_name: str | None = None,
93
+ ) -> dict[str, Any]:
94
+ """
95
+ Record a deposit for a vault.
96
+
97
+ POST /api/v1/public/vaults/deposits/
98
+ """
99
+ url = f"{self.api_base_url}/public/vaults/deposits/"
100
+ payload: dict[str, Any] = {
101
+ "wallet_address": wallet_address,
102
+ "chain_id": chain_id,
103
+ "token_address": token_address,
104
+ "token_amount": str(token_amount),
105
+ "usd_value": str(usd_value),
106
+ "data": data or {},
107
+ }
108
+ if strategy_name is not None:
109
+ payload["strategy_name"] = strategy_name
110
+ response = await self._authed_request("POST", url, json=payload)
111
+ data_resp = response.json()
112
+ return data_resp.get("data", data_resp)
113
+
114
+ async def add_vault_withdraw(
115
+ self,
116
+ *,
117
+ wallet_address: str,
118
+ chain_id: int,
119
+ token_address: str,
120
+ token_amount: str | float,
121
+ usd_value: str | float,
122
+ data: dict[str, Any] | None = None,
123
+ strategy_name: str | None = None,
124
+ ) -> dict[str, Any]:
125
+ """
126
+ Record a withdrawal for a vault.
127
+
128
+ POST /api/v1/public/vaults/withdrawals/
129
+ """
130
+ url = f"{self.api_base_url}/public/vaults/withdrawals/"
131
+ payload: dict[str, Any] = {
132
+ "wallet_address": wallet_address,
133
+ "chain_id": chain_id,
134
+ "token_address": token_address,
135
+ "token_amount": str(token_amount),
136
+ "usd_value": str(usd_value),
137
+ "data": data or {},
138
+ }
139
+ if strategy_name is not None:
140
+ payload["strategy_name"] = strategy_name
141
+ response = await self._authed_request("POST", url, json=payload)
142
+ data_resp = response.json()
143
+ return data_resp.get("data", data_resp)
144
+
145
+ async def add_vault_operation(
146
+ self,
147
+ *,
148
+ wallet_address: str,
149
+ operation_data: dict[str, Any],
150
+ usd_value: str | float,
151
+ strategy_name: str | None = None,
152
+ ) -> dict[str, Any]:
153
+ """
154
+ Record a vault operation (e.g., swaps, rebalances) for bookkeeping.
155
+
156
+ POST /api/v1/public/vaults/operations/
157
+ """
158
+ url = f"{self.api_base_url}/public/vaults/operations/"
159
+ payload: dict[str, Any] = {
160
+ "wallet_address": wallet_address,
161
+ "operation_data": operation_data,
162
+ "usd_value": str(usd_value),
163
+ }
164
+ if strategy_name is not None:
165
+ payload["strategy_name"] = strategy_name
166
+ response = await self._authed_request("POST", url, json=payload)
167
+ data_resp = response.json()
168
+ return data_resp.get("data", data_resp)
169
+
170
+ async def add_vault_cashflow(
171
+ self,
172
+ *,
173
+ wallet_address: str,
174
+ block_timestamp: int,
175
+ token_addr: str,
176
+ amount: str | int | float,
177
+ description: str,
178
+ strategy_name: str | None = None,
179
+ ) -> dict[str, Any]:
180
+ """
181
+ Record a cashflow for a vault (interest, funding, reward, or fee).
182
+
183
+ POST /api/v1/public/vaults/cashflows/
184
+
185
+ Args:
186
+ wallet_address: Vault wallet address
187
+ block_timestamp: Block timestamp (Unix timestamp)
188
+ token_addr: Token contract address
189
+ amount: Cashflow amount (in token units)
190
+ description: Cashflow type - must be one of: "interest", "funding", "reward", "fee", "lend", "unlend", "borrow"
191
+ strategy_name: Optional strategy name
192
+
193
+ Returns:
194
+ Dict containing the cashflow record or error details
195
+ """
196
+ valid_descriptions = [
197
+ "interest",
198
+ "funding",
199
+ "reward",
200
+ "fee",
201
+ "lend",
202
+ "unlend",
203
+ "borrow",
204
+ ]
205
+ if description not in valid_descriptions:
206
+ raise ValueError(
207
+ f"Invalid description '{description}'. Must be one of: {valid_descriptions}"
208
+ )
209
+
210
+ url = f"{self.api_base_url}/public/vaults/cashflows/"
211
+ payload: dict[str, Any] = {
212
+ "wallet_address": wallet_address,
213
+ "block_timestamp": block_timestamp,
214
+ "token_addr": token_addr,
215
+ "amount": str(amount),
216
+ "description": description,
217
+ }
218
+ if strategy_name is not None:
219
+ payload["strategy_name"] = strategy_name
220
+ response = await self._authed_request("POST", url, json=payload)
221
+ data_resp = response.json()
222
+ return data_resp.get("data", data_resp)