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.
- wayfinder_paths/CONFIG_GUIDE.md +399 -0
- wayfinder_paths/__init__.py +22 -0
- wayfinder_paths/abis/generic/erc20.json +383 -0
- wayfinder_paths/adapters/__init__.py +0 -0
- wayfinder_paths/adapters/balance_adapter/README.md +94 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
- wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
- wayfinder_paths/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
- wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
- wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
- wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
- wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
- wayfinder_paths/adapters/pool_adapter/README.md +206 -0
- wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
- wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
- wayfinder_paths/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
- wayfinder_paths/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
- wayfinder_paths/config.example.json +22 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +18 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/adapters/models.py +46 -0
- wayfinder_paths/core/analytics/__init__.py +11 -0
- wayfinder_paths/core/analytics/bootstrap.py +57 -0
- wayfinder_paths/core/analytics/stats.py +48 -0
- wayfinder_paths/core/analytics/test_analytics.py +170 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +109 -0
- wayfinder_paths/core/clients/ClientManager.py +210 -0
- wayfinder_paths/core/clients/HyperlendClient.py +192 -0
- wayfinder_paths/core/clients/LedgerClient.py +443 -0
- wayfinder_paths/core/clients/PoolClient.py +128 -0
- wayfinder_paths/core/clients/SimulationClient.py +192 -0
- wayfinder_paths/core/clients/TokenClient.py +89 -0
- wayfinder_paths/core/clients/TransactionClient.py +63 -0
- wayfinder_paths/core/clients/WalletClient.py +94 -0
- wayfinder_paths/core/clients/WayfinderClient.py +269 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +392 -0
- wayfinder_paths/core/clients/sdk_example.py +110 -0
- wayfinder_paths/core/config.py +458 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +42 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/StrategyJob.py +188 -0
- wayfinder_paths/core/engine/__init__.py +5 -0
- wayfinder_paths/core/engine/manifest.py +97 -0
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +179 -0
- wayfinder_paths/core/services/local_evm_txn.py +430 -0
- wayfinder_paths/core/services/local_token_txn.py +231 -0
- wayfinder_paths/core/services/web3_service.py +45 -0
- wayfinder_paths/core/settings.py +61 -0
- wayfinder_paths/core/strategies/Strategy.py +280 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/strategies/descriptors.py +81 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +206 -0
- wayfinder_paths/core/utils/wallets.py +77 -0
- wayfinder_paths/core/wallets/README.md +91 -0
- wayfinder_paths/core/wallets/WalletManager.py +56 -0
- wayfinder_paths/core/wallets/__init__.py +7 -0
- wayfinder_paths/policies/enso.py +17 -0
- wayfinder_paths/policies/erc20.py +34 -0
- wayfinder_paths/policies/evm.py +21 -0
- wayfinder_paths/policies/hyper_evm.py +19 -0
- wayfinder_paths/policies/hyperlend.py +12 -0
- wayfinder_paths/policies/hyperliquid.py +30 -0
- wayfinder_paths/policies/moonwell.py +54 -0
- wayfinder_paths/policies/prjx.py +30 -0
- wayfinder_paths/policies/util.py +27 -0
- wayfinder_paths/run_strategy.py +411 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +169 -0
- wayfinder_paths/scripts/run_strategy.py +124 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/strategies/__init__.py +0 -0
- wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
- wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
- wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
- wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
- wayfinder_paths/strategies/config.py +85 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
- wayfinder_paths/templates/adapter/README.md +105 -0
- wayfinder_paths/templates/adapter/adapter.py +26 -0
- wayfinder_paths/templates/adapter/examples.json +8 -0
- wayfinder_paths/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/templates/strategy/README.md +153 -0
- wayfinder_paths/templates/strategy/examples.json +11 -0
- wayfinder_paths/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/templates/strategy/strategy.py +57 -0
- wayfinder_paths/templates/strategy/test_strategy.py +197 -0
- wayfinder_paths/tests/__init__.py +0 -0
- wayfinder_paths/tests/test_smoke_manifest.py +48 -0
- wayfinder_paths/tests/test_test_coverage.py +212 -0
- wayfinder_paths/tests/test_utils.py +64 -0
- wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
- wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
- 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
|
+
]
|