wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.24__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/adapters/balance_adapter/adapter.py +250 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +1 -1
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -2
- wayfinder_paths/core/clients/protocols.py +21 -22
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +12 -0
- wayfinder_paths/core/constants/__init__.py +15 -0
- wayfinder_paths/core/constants/base.py +6 -1
- wayfinder_paths/core/constants/contracts.py +39 -26
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -61
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/transaction.py +44 -1
- wayfinder_paths/core/utils/web3.py +3 -0
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
- wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
from unittest.mock import AsyncMock, patch
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
from web3 import Web3
|
|
11
|
+
|
|
12
|
+
from wayfinder_paths.adapters.pendle_adapter.adapter import PendleAdapter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestPendleAdapter:
|
|
16
|
+
def test_adapter_type(self):
|
|
17
|
+
adapter = PendleAdapter(config={})
|
|
18
|
+
assert adapter.adapter_type == "PENDLE"
|
|
19
|
+
|
|
20
|
+
@pytest.mark.asyncio
|
|
21
|
+
async def test_list_active_pt_yt_markets_filters_and_sort(self, monkeypatch):
|
|
22
|
+
fixed_now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
23
|
+
monkeypatch.setattr(
|
|
24
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter._now_utc",
|
|
25
|
+
lambda: fixed_now,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def markets_payload(chain_id: int) -> dict[str, Any]:
|
|
29
|
+
if chain_id == 42161:
|
|
30
|
+
return {
|
|
31
|
+
"markets": [
|
|
32
|
+
{
|
|
33
|
+
"chainId": 42161,
|
|
34
|
+
"name": "PT-USDC-Example",
|
|
35
|
+
"address": "42161-0xMarketA",
|
|
36
|
+
"pt": "42161-0xPTA",
|
|
37
|
+
"yt": "42161-0xYTA",
|
|
38
|
+
"sy": "42161-0xSYA",
|
|
39
|
+
"underlyingAsset": "42161-0xUSDC",
|
|
40
|
+
"expiry": "2026-02-01T00:00:00.000Z",
|
|
41
|
+
"details": {
|
|
42
|
+
"liquidity": 300_000,
|
|
43
|
+
"tradingVolume": 40_000,
|
|
44
|
+
"totalTvl": 500_000,
|
|
45
|
+
"impliedApy": 0.12,
|
|
46
|
+
"underlyingApy": 0.15,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"chainId": 42161,
|
|
51
|
+
"name": "TooSmallLiquidity",
|
|
52
|
+
"address": "42161-0xMarketB",
|
|
53
|
+
"pt": "42161-0xPTB",
|
|
54
|
+
"yt": "42161-0xYTB",
|
|
55
|
+
"sy": "42161-0xSYB",
|
|
56
|
+
"underlyingAsset": "42161-0xUSDT",
|
|
57
|
+
"expiry": "2026-02-01T00:00:00.000Z",
|
|
58
|
+
"details": {
|
|
59
|
+
"liquidity": 10_000,
|
|
60
|
+
"tradingVolume": 999_999,
|
|
61
|
+
"totalTvl": 500_000,
|
|
62
|
+
"impliedApy": 0.50,
|
|
63
|
+
"underlyingApy": 0.55,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
if chain_id == 8453:
|
|
69
|
+
return {
|
|
70
|
+
"markets": [
|
|
71
|
+
{
|
|
72
|
+
"chainId": 8453,
|
|
73
|
+
"name": "PT-DAI-Example",
|
|
74
|
+
"address": "8453-0xMarketC",
|
|
75
|
+
"pt": "8453-0xPTC",
|
|
76
|
+
"yt": "8453-0xYTC",
|
|
77
|
+
"sy": "8453-0xSYC",
|
|
78
|
+
"underlyingAsset": "8453-0xDAI",
|
|
79
|
+
"expiry": "2026-03-15T00:00:00.000Z",
|
|
80
|
+
"details": {
|
|
81
|
+
"liquidity": 800_000,
|
|
82
|
+
"tradingVolume": 30_000,
|
|
83
|
+
"totalTvl": 1_100_000,
|
|
84
|
+
"impliedApy": 0.10,
|
|
85
|
+
"underlyingApy": 0.13,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
return {"markets": []}
|
|
91
|
+
|
|
92
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
93
|
+
assert request.url.path == "/core/v1/markets/all"
|
|
94
|
+
chain_id = int(request.url.params.get("chainId", "0"))
|
|
95
|
+
assert request.url.params.get("isActive") == "true"
|
|
96
|
+
return httpx.Response(200, json=markets_payload(chain_id))
|
|
97
|
+
|
|
98
|
+
client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
|
99
|
+
adapter = PendleAdapter(config={}, client=client)
|
|
100
|
+
|
|
101
|
+
rows = await adapter.list_active_pt_yt_markets(
|
|
102
|
+
chains=["arbitrum", "base"],
|
|
103
|
+
min_liquidity_usd=250_000,
|
|
104
|
+
min_volume_usd_24h=25_000,
|
|
105
|
+
min_days_to_expiry=7,
|
|
106
|
+
sort_by="fixed_apy",
|
|
107
|
+
descending=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
await client.aclose()
|
|
111
|
+
|
|
112
|
+
# TooSmallLiquidity filtered out; remaining rows sorted by fixedApy desc
|
|
113
|
+
assert [r["marketAddress"] for r in rows] == ["0xMarketA", "0xMarketC"]
|
|
114
|
+
assert rows[0]["fixedApy"] == 0.12
|
|
115
|
+
assert rows[0]["floatingApy"] == pytest.approx(0.03)
|
|
116
|
+
assert rows[0]["underlyingAddress"] == "0xUSDC"
|
|
117
|
+
|
|
118
|
+
@pytest.mark.asyncio
|
|
119
|
+
async def test_sdk_swap_v2_builds_query_params(self):
|
|
120
|
+
captured: dict[str, Any] = {}
|
|
121
|
+
|
|
122
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
123
|
+
captured["path"] = request.url.path
|
|
124
|
+
captured["params"] = dict(request.url.params)
|
|
125
|
+
return httpx.Response(
|
|
126
|
+
200,
|
|
127
|
+
json={
|
|
128
|
+
"tx": {"to": "0xRouter", "data": "0xdeadbeef", "value": "0"},
|
|
129
|
+
"tokenApprovals": [],
|
|
130
|
+
"data": {"amountOut": "123"},
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
|
135
|
+
adapter = PendleAdapter(
|
|
136
|
+
config={}, client=client, base_url="https://api-v2.pendle.finance/core"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
market = "0xMarketA"
|
|
140
|
+
await adapter.sdk_swap_v2(
|
|
141
|
+
chain="arbitrum",
|
|
142
|
+
market_address=market,
|
|
143
|
+
receiver="0xReceiver",
|
|
144
|
+
slippage=0.01,
|
|
145
|
+
token_in="0xTokenIn",
|
|
146
|
+
token_out="0xTokenOut",
|
|
147
|
+
amount_in="1000",
|
|
148
|
+
enable_aggregator=True,
|
|
149
|
+
aggregators=["one", "two"],
|
|
150
|
+
additional_data=["impliedApy", "effectiveApy"],
|
|
151
|
+
need_scale=True,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
await client.aclose()
|
|
155
|
+
|
|
156
|
+
assert captured["path"] == f"/core/v2/sdk/42161/markets/{market}/swap"
|
|
157
|
+
params = captured["params"]
|
|
158
|
+
assert params["receiver"] == "0xReceiver"
|
|
159
|
+
assert params["slippage"] == "0.01"
|
|
160
|
+
assert params["enableAggregator"] == "true"
|
|
161
|
+
assert params["aggregators"] == "one,two"
|
|
162
|
+
assert params["tokenIn"] == "0xTokenIn"
|
|
163
|
+
assert params["tokenOut"] == "0xTokenOut"
|
|
164
|
+
assert params["amountIn"] == "1000"
|
|
165
|
+
assert params["additionalData"] == "impliedApy,effectiveApy"
|
|
166
|
+
assert params["needScale"] == "true"
|
|
167
|
+
|
|
168
|
+
@pytest.mark.asyncio
|
|
169
|
+
async def test_sdk_convert_v2_falls_back_to_get_and_builds_query_params(self):
|
|
170
|
+
captured: dict[str, Any] = {"methods": []}
|
|
171
|
+
|
|
172
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
173
|
+
captured["methods"].append(request.method)
|
|
174
|
+
|
|
175
|
+
if request.method == "POST":
|
|
176
|
+
# Pendle currently responds 404 for POST in some environments.
|
|
177
|
+
return httpx.Response(
|
|
178
|
+
404,
|
|
179
|
+
json={"message": "Not Found"},
|
|
180
|
+
headers={"x-ratelimit-remaining": "98"},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
captured["path"] = request.url.path
|
|
184
|
+
captured["params"] = dict(request.url.params)
|
|
185
|
+
return httpx.Response(
|
|
186
|
+
200,
|
|
187
|
+
json={
|
|
188
|
+
"action": "swap",
|
|
189
|
+
"inputs": [{"token": "0xTokenIn", "amount": "1000"}],
|
|
190
|
+
"requiredApprovals": [{"token": "0xTokenIn", "amount": "1000"}],
|
|
191
|
+
"routes": [
|
|
192
|
+
{
|
|
193
|
+
"tx": {
|
|
194
|
+
"to": "0xRouter",
|
|
195
|
+
"from": "0xFrom",
|
|
196
|
+
"data": "0xdead",
|
|
197
|
+
"value": "0",
|
|
198
|
+
},
|
|
199
|
+
"outputs": [{"token": "0xTokenOut", "amount": "123"}],
|
|
200
|
+
"data": {"effectiveApy": 0.1, "priceImpact": 0.01},
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
headers={"x-computing-unit": "6", "x-ratelimit-remaining": "99"},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
|
208
|
+
adapter = PendleAdapter(
|
|
209
|
+
config={"strategy_wallet": {"address": "0x" + "a" * 40}},
|
|
210
|
+
client=client,
|
|
211
|
+
base_url="https://api-v2.pendle.finance/core",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
resp = await adapter.sdk_convert_v2(
|
|
215
|
+
chain="arbitrum",
|
|
216
|
+
slippage=0.01,
|
|
217
|
+
receiver="0xReceiver",
|
|
218
|
+
inputs=[{"token": "0xTokenIn", "amount": "1000"}],
|
|
219
|
+
outputs=["0xTokenOut"],
|
|
220
|
+
enable_aggregator=True,
|
|
221
|
+
aggregators=["kyberswap"],
|
|
222
|
+
additional_data=["impliedApy", "effectiveApy", "priceImpact"],
|
|
223
|
+
need_scale=True,
|
|
224
|
+
use_limit_order=True,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
await client.aclose()
|
|
228
|
+
|
|
229
|
+
assert captured["methods"] == ["POST", "GET"]
|
|
230
|
+
assert captured["path"] == "/core/v2/sdk/42161/convert"
|
|
231
|
+
params = captured["params"]
|
|
232
|
+
assert params["receiver"] == "0xReceiver"
|
|
233
|
+
assert params["slippage"] == "0.01"
|
|
234
|
+
assert params["tokensIn"] == "0xTokenIn"
|
|
235
|
+
assert params["amountsIn"] == "1000"
|
|
236
|
+
assert params["tokensOut"] == "0xTokenOut"
|
|
237
|
+
assert params["enableAggregator"] == "true"
|
|
238
|
+
assert params["aggregators"] == "kyberswap"
|
|
239
|
+
assert params["additionalData"] == "impliedApy,effectiveApy,priceImpact"
|
|
240
|
+
assert params["needScale"] == "true"
|
|
241
|
+
assert params["useLimitOrder"] == "true"
|
|
242
|
+
assert resp["rateLimit"]["computingUnit"] == 6
|
|
243
|
+
|
|
244
|
+
def test_build_convert_plan_selects_best_route(self):
|
|
245
|
+
adapter = PendleAdapter(
|
|
246
|
+
config={"strategy_wallet": {"address": "0x" + "a" * 40}}
|
|
247
|
+
)
|
|
248
|
+
convert_response = {
|
|
249
|
+
"action": "swap",
|
|
250
|
+
"requiredApprovals": [{"token": "0xTokenIn", "amount": "1000"}],
|
|
251
|
+
"routes": [
|
|
252
|
+
{
|
|
253
|
+
"tx": {
|
|
254
|
+
"to": "0x" + "b" * 40,
|
|
255
|
+
"from": "0x" + "a" * 40,
|
|
256
|
+
"data": "0x01",
|
|
257
|
+
"value": "0",
|
|
258
|
+
},
|
|
259
|
+
"outputs": [{"token": "0xTokenOut", "amount": "100"}],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"tx": {
|
|
263
|
+
"to": "0x" + "c" * 40,
|
|
264
|
+
"from": "0x" + "a" * 40,
|
|
265
|
+
"data": "0x02",
|
|
266
|
+
"value": "0",
|
|
267
|
+
},
|
|
268
|
+
"outputs": [{"token": "0xTokenOut", "amount": "200"}],
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
plan = adapter.build_convert_plan(
|
|
274
|
+
chain=42161, convert_response=convert_response
|
|
275
|
+
)
|
|
276
|
+
assert plan["tx"]["to"].lower() == ("0x" + "c" * 40).lower()
|
|
277
|
+
assert plan["outputs"][0]["amount"] == "200"
|
|
278
|
+
assert plan["approvals"][0]["token"] == "0xTokenIn"
|
|
279
|
+
|
|
280
|
+
@pytest.mark.asyncio
|
|
281
|
+
async def test_execute_convert_success(self):
|
|
282
|
+
signing_callback = AsyncMock(return_value=b"\x00" * 65)
|
|
283
|
+
|
|
284
|
+
adapter = PendleAdapter(
|
|
285
|
+
config={"strategy_wallet": {"address": "0x" + "a" * 40}},
|
|
286
|
+
strategy_wallet_signing_callback=signing_callback,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
adapter.sdk_convert_v2 = AsyncMock(
|
|
290
|
+
return_value={
|
|
291
|
+
"action": "swap",
|
|
292
|
+
"requiredApprovals": [{"token": "0x" + "c" * 40, "amount": "1000"}],
|
|
293
|
+
"routes": [
|
|
294
|
+
{
|
|
295
|
+
"tx": {
|
|
296
|
+
"to": "0x" + "b" * 40,
|
|
297
|
+
"from": "0x" + "a" * 40,
|
|
298
|
+
"data": "0xdead",
|
|
299
|
+
"value": "0",
|
|
300
|
+
},
|
|
301
|
+
"outputs": [{"token": "0x" + "d" * 40, "amount": "123"}],
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
with (
|
|
308
|
+
patch(
|
|
309
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.get_token_balance",
|
|
310
|
+
new_callable=AsyncMock,
|
|
311
|
+
return_value=10_000,
|
|
312
|
+
),
|
|
313
|
+
patch.object(
|
|
314
|
+
adapter,
|
|
315
|
+
"_ensure_allowance",
|
|
316
|
+
new_callable=AsyncMock,
|
|
317
|
+
return_value=(True, {"status": "already_approved"}),
|
|
318
|
+
),
|
|
319
|
+
patch.object(
|
|
320
|
+
adapter,
|
|
321
|
+
"_send_tx",
|
|
322
|
+
new_callable=AsyncMock,
|
|
323
|
+
return_value=(True, "0xtxhash123"),
|
|
324
|
+
),
|
|
325
|
+
):
|
|
326
|
+
ok, res = await adapter.execute_convert(
|
|
327
|
+
chain="arbitrum",
|
|
328
|
+
slippage=0.01,
|
|
329
|
+
inputs=[{"token": "0x" + "c" * 40, "amount": "1000"}],
|
|
330
|
+
outputs=["0x" + "d" * 40],
|
|
331
|
+
receiver="0x" + "a" * 40,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
assert ok is True
|
|
335
|
+
assert res["tx_hash"] == "0xtxhash123"
|
|
336
|
+
assert res["chainId"] == 42161
|
|
337
|
+
|
|
338
|
+
@pytest.mark.asyncio
|
|
339
|
+
async def test_build_best_pt_swap_tx_selects_best_effective_apy(self):
|
|
340
|
+
adapter = PendleAdapter(config={})
|
|
341
|
+
adapter.list_active_pt_yt_markets = AsyncMock(
|
|
342
|
+
return_value=[
|
|
343
|
+
{
|
|
344
|
+
"marketAddress": "0xM1",
|
|
345
|
+
"ptAddress": "0xPT1",
|
|
346
|
+
"fixedApy": 0.10,
|
|
347
|
+
"liquidityUsd": 500_000,
|
|
348
|
+
"volumeUsd24h": 100_000,
|
|
349
|
+
"daysToExpiry": 30.0,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"marketAddress": "0xM2",
|
|
353
|
+
"ptAddress": "0xPT2",
|
|
354
|
+
"fixedApy": 0.12,
|
|
355
|
+
"liquidityUsd": 500_000,
|
|
356
|
+
"volumeUsd24h": 100_000,
|
|
357
|
+
"daysToExpiry": 30.0,
|
|
358
|
+
},
|
|
359
|
+
]
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
async def fake_swap(*, market_address: str, **_: Any) -> dict[str, Any]:
|
|
363
|
+
if market_address == "0xM1":
|
|
364
|
+
return {
|
|
365
|
+
"tx": {"to": "0xRouter", "data": "0x1"},
|
|
366
|
+
"data": {"effectiveApy": 0.08, "priceImpact": 0.001},
|
|
367
|
+
"tokenApprovals": [],
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
"tx": {"to": "0xRouter", "data": "0x2"},
|
|
371
|
+
"data": {"effectiveApy": 0.09, "priceImpact": 0.005},
|
|
372
|
+
"tokenApprovals": [],
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
adapter.sdk_swap_v2 = AsyncMock(side_effect=fake_swap)
|
|
376
|
+
|
|
377
|
+
best = await adapter.build_best_pt_swap_tx(
|
|
378
|
+
chain=42161,
|
|
379
|
+
token_in="0xTokenIn",
|
|
380
|
+
amount_in="1000",
|
|
381
|
+
receiver="0xReceiver",
|
|
382
|
+
max_markets_to_quote=2,
|
|
383
|
+
prefer="effective_apy",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
assert best["ok"] is True
|
|
387
|
+
assert best["selectedMarket"]["marketAddress"] == "0xM2"
|
|
388
|
+
assert best["quote"]["effectiveApy"] == 0.09
|
|
389
|
+
|
|
390
|
+
@pytest.mark.asyncio
|
|
391
|
+
async def test_get_full_user_state_onchain_multicall_filters_zeros(
|
|
392
|
+
self, monkeypatch
|
|
393
|
+
):
|
|
394
|
+
w3 = Web3()
|
|
395
|
+
user = "0x" + ("a" * 40)
|
|
396
|
+
market1 = "0x" + ("1" * 40)
|
|
397
|
+
pt1 = "0x" + ("2" * 40)
|
|
398
|
+
yt1 = "0x" + ("3" * 40)
|
|
399
|
+
sy1 = "0x" + ("4" * 40)
|
|
400
|
+
u1 = "0x" + ("5" * 40)
|
|
401
|
+
|
|
402
|
+
market2 = "0x" + ("6" * 40)
|
|
403
|
+
pt2 = "0x" + ("7" * 40)
|
|
404
|
+
yt2 = "0x" + ("8" * 40)
|
|
405
|
+
sy2 = "0x" + ("9" * 40)
|
|
406
|
+
u2 = "0x" + ("b" * 40)
|
|
407
|
+
|
|
408
|
+
@asynccontextmanager
|
|
409
|
+
async def mock_web3_ctx(_chain_id):
|
|
410
|
+
yield w3
|
|
411
|
+
|
|
412
|
+
adapter = PendleAdapter(config={})
|
|
413
|
+
adapter.fetch_markets = AsyncMock(
|
|
414
|
+
return_value={
|
|
415
|
+
"markets": [
|
|
416
|
+
{
|
|
417
|
+
"chainId": 42161,
|
|
418
|
+
"name": "M1",
|
|
419
|
+
"address": f"42161-{market1}",
|
|
420
|
+
"pt": f"42161-{pt1}",
|
|
421
|
+
"yt": f"42161-{yt1}",
|
|
422
|
+
"sy": f"42161-{sy1}",
|
|
423
|
+
"underlyingAsset": f"42161-{u1}",
|
|
424
|
+
"expiry": "2026-02-01T00:00:00.000Z",
|
|
425
|
+
"isActive": True,
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"chainId": 42161,
|
|
429
|
+
"name": "M2",
|
|
430
|
+
"address": f"42161-{market2}",
|
|
431
|
+
"pt": f"42161-{pt2}",
|
|
432
|
+
"yt": f"42161-{yt2}",
|
|
433
|
+
"sy": f"42161-{sy2}",
|
|
434
|
+
"underlyingAsset": f"42161-{u2}",
|
|
435
|
+
"expiry": "2026-02-01T00:00:00.000Z",
|
|
436
|
+
"isActive": True,
|
|
437
|
+
},
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Order is: (pt bal, pt dec, yt bal, yt dec, lp bal, lp dec, sy bal, sy dec) per market.
|
|
443
|
+
adapter._multicall_uint256_chunked = AsyncMock(
|
|
444
|
+
return_value=[
|
|
445
|
+
1000,
|
|
446
|
+
18,
|
|
447
|
+
0,
|
|
448
|
+
18,
|
|
449
|
+
0,
|
|
450
|
+
18,
|
|
451
|
+
0,
|
|
452
|
+
18,
|
|
453
|
+
0,
|
|
454
|
+
18,
|
|
455
|
+
0,
|
|
456
|
+
18,
|
|
457
|
+
0,
|
|
458
|
+
18,
|
|
459
|
+
0,
|
|
460
|
+
18,
|
|
461
|
+
]
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
monkeypatch.setattr(
|
|
465
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.web3_from_chain_id",
|
|
466
|
+
mock_web3_ctx,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
ok, state = await adapter.get_full_user_state(chain=42161, account=user)
|
|
470
|
+
assert ok is True
|
|
471
|
+
assert state["protocol"] == "pendle"
|
|
472
|
+
assert state["chainId"] == 42161
|
|
473
|
+
assert state["account"] == user
|
|
474
|
+
assert len(state["positions"]) == 1
|
|
475
|
+
assert state["positions"][0]["marketAddress"] == market1
|
|
476
|
+
|
|
477
|
+
@pytest.mark.asyncio
|
|
478
|
+
async def test_execute_swap_success(self):
|
|
479
|
+
"""Test successful swap execution."""
|
|
480
|
+
signing_callback = AsyncMock(return_value=b"\x00" * 65)
|
|
481
|
+
|
|
482
|
+
router_addr = "0x" + "b" * 40
|
|
483
|
+
token_in_addr = "0x" + "c" * 40
|
|
484
|
+
|
|
485
|
+
adapter = PendleAdapter(
|
|
486
|
+
config={"strategy_wallet": {"address": "0x" + "a" * 40}},
|
|
487
|
+
strategy_wallet_signing_callback=signing_callback,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Mock sdk_swap_v2 to return a valid quote
|
|
491
|
+
adapter.sdk_swap_v2 = AsyncMock(
|
|
492
|
+
return_value={
|
|
493
|
+
"tx": {"to": router_addr, "data": "0xdeadbeef", "value": "0"},
|
|
494
|
+
"tokenApprovals": [{"token": token_in_addr, "amount": "1000000"}],
|
|
495
|
+
"data": {"amountOut": "990000", "priceImpact": 0.001},
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Mock allowance and approval
|
|
500
|
+
with (
|
|
501
|
+
patch(
|
|
502
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.get_token_allowance",
|
|
503
|
+
new_callable=AsyncMock,
|
|
504
|
+
return_value=2**256 - 1, # Already approved
|
|
505
|
+
),
|
|
506
|
+
patch(
|
|
507
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.send_transaction",
|
|
508
|
+
new_callable=AsyncMock,
|
|
509
|
+
return_value="0xtxhash123",
|
|
510
|
+
) as mock_send,
|
|
511
|
+
):
|
|
512
|
+
success, result = await adapter.execute_swap(
|
|
513
|
+
chain="arbitrum",
|
|
514
|
+
market_address="0x" + "d" * 40,
|
|
515
|
+
token_in=token_in_addr,
|
|
516
|
+
token_out="0x" + "e" * 40,
|
|
517
|
+
amount_in="1000000",
|
|
518
|
+
slippage=0.01,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
assert success is True
|
|
522
|
+
assert result["tx_hash"] == "0xtxhash123"
|
|
523
|
+
assert result["chainId"] == 42161
|
|
524
|
+
assert "quote" in result
|
|
525
|
+
mock_send.assert_called_once()
|
|
526
|
+
|
|
527
|
+
@pytest.mark.asyncio
|
|
528
|
+
async def test_execute_swap_quote_fails(self):
|
|
529
|
+
"""Test swap fails when quote returns invalid tx."""
|
|
530
|
+
adapter = PendleAdapter(
|
|
531
|
+
config={"strategy_wallet": {"address": "0x" + "a" * 40}},
|
|
532
|
+
strategy_wallet_signing_callback=AsyncMock(),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
adapter.sdk_swap_v2 = AsyncMock(
|
|
536
|
+
return_value={"tx": None, "tokenApprovals": [], "data": {}}
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
success, result = await adapter.execute_swap(
|
|
540
|
+
chain="arbitrum",
|
|
541
|
+
market_address="0xMarket",
|
|
542
|
+
token_in="0xTokenIn",
|
|
543
|
+
token_out="0xPT",
|
|
544
|
+
amount_in="1000000",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
assert success is False
|
|
548
|
+
assert result["stage"] == "quote"
|
|
549
|
+
assert "error" in result
|
|
550
|
+
|
|
551
|
+
@pytest.mark.asyncio
|
|
552
|
+
async def test_execute_swap_approval_fails(self):
|
|
553
|
+
"""Test swap fails when approval fails."""
|
|
554
|
+
router_addr = "0x" + "b" * 40
|
|
555
|
+
token_in_addr = "0x" + "c" * 40
|
|
556
|
+
|
|
557
|
+
adapter = PendleAdapter(
|
|
558
|
+
config={"strategy_wallet": {"address": "0x" + "a" * 40}},
|
|
559
|
+
strategy_wallet_signing_callback=AsyncMock(return_value=b"\x00" * 65),
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
adapter.sdk_swap_v2 = AsyncMock(
|
|
563
|
+
return_value={
|
|
564
|
+
"tx": {"to": router_addr, "data": "0xdeadbeef"},
|
|
565
|
+
"tokenApprovals": [{"token": token_in_addr, "amount": "1000000"}],
|
|
566
|
+
"data": {},
|
|
567
|
+
}
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
with (
|
|
571
|
+
patch(
|
|
572
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.get_token_allowance",
|
|
573
|
+
new_callable=AsyncMock,
|
|
574
|
+
return_value=0, # No allowance
|
|
575
|
+
),
|
|
576
|
+
patch(
|
|
577
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.build_approve_transaction",
|
|
578
|
+
new_callable=AsyncMock,
|
|
579
|
+
return_value={
|
|
580
|
+
"to": token_in_addr,
|
|
581
|
+
"data": "0x",
|
|
582
|
+
"chainId": 42161,
|
|
583
|
+
"from": "0x" + "a" * 40,
|
|
584
|
+
},
|
|
585
|
+
),
|
|
586
|
+
patch(
|
|
587
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.send_transaction",
|
|
588
|
+
new_callable=AsyncMock,
|
|
589
|
+
side_effect=Exception("Approval tx failed"),
|
|
590
|
+
),
|
|
591
|
+
):
|
|
592
|
+
success, result = await adapter.execute_swap(
|
|
593
|
+
chain="arbitrum",
|
|
594
|
+
market_address="0x" + "d" * 40,
|
|
595
|
+
token_in=token_in_addr,
|
|
596
|
+
token_out="0x" + "e" * 40,
|
|
597
|
+
amount_in="1000000",
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
assert success is False
|
|
601
|
+
assert result["stage"] == "approval"
|
|
602
|
+
|
|
603
|
+
@pytest.mark.asyncio
|
|
604
|
+
async def test_execute_swap_requires_signing_callback(self):
|
|
605
|
+
"""Test swap fails without signing callback."""
|
|
606
|
+
router_addr = "0x" + "b" * 40
|
|
607
|
+
token_in_addr = "0x" + "c" * 40
|
|
608
|
+
|
|
609
|
+
adapter = PendleAdapter(
|
|
610
|
+
config={"strategy_wallet": {"address": "0x" + "a" * 40}},
|
|
611
|
+
# No signing callback
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Include tokenApprovals so it fails during the approval stage
|
|
615
|
+
adapter.sdk_swap_v2 = AsyncMock(
|
|
616
|
+
return_value={
|
|
617
|
+
"tx": {"to": router_addr, "data": "0xdeadbeef"},
|
|
618
|
+
"tokenApprovals": [{"token": token_in_addr, "amount": "1000000"}],
|
|
619
|
+
"data": {},
|
|
620
|
+
}
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
with (
|
|
624
|
+
patch(
|
|
625
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.get_token_allowance",
|
|
626
|
+
new_callable=AsyncMock,
|
|
627
|
+
return_value=0, # No allowance, will try to approve
|
|
628
|
+
),
|
|
629
|
+
patch(
|
|
630
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.build_approve_transaction",
|
|
631
|
+
new_callable=AsyncMock,
|
|
632
|
+
return_value={
|
|
633
|
+
"to": token_in_addr,
|
|
634
|
+
"data": "0xapprove",
|
|
635
|
+
"chainId": 42161,
|
|
636
|
+
},
|
|
637
|
+
),
|
|
638
|
+
):
|
|
639
|
+
success, result = await adapter.execute_swap(
|
|
640
|
+
chain="arbitrum",
|
|
641
|
+
market_address="0x" + "d" * 40,
|
|
642
|
+
token_in=token_in_addr,
|
|
643
|
+
token_out="0x" + "e" * 40,
|
|
644
|
+
amount_in="1000000",
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
assert success is False
|
|
648
|
+
assert result["stage"] == "approval"
|
|
649
|
+
assert "strategy_wallet_signing_callback" in result["details"]["error"]
|
|
650
|
+
|
|
651
|
+
@pytest.mark.asyncio
|
|
652
|
+
async def test_execute_swap_requires_strategy_wallet(self):
|
|
653
|
+
"""Test swap fails without strategy_wallet address."""
|
|
654
|
+
adapter = PendleAdapter(
|
|
655
|
+
config={}, # No strategy_wallet
|
|
656
|
+
strategy_wallet_signing_callback=AsyncMock(),
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
with pytest.raises(ValueError, match="strategy_wallet address is required"):
|
|
660
|
+
await adapter.execute_swap(
|
|
661
|
+
chain="arbitrum",
|
|
662
|
+
market_address="0xMarket",
|
|
663
|
+
token_in="0xTokenIn",
|
|
664
|
+
token_out="0xPT",
|
|
665
|
+
amount_in="1000000",
|
|
666
|
+
)
|
|
@@ -11,10 +11,6 @@
|
|
|
11
11
|
"description": "Proper error handling for various scenarios",
|
|
12
12
|
"code": "from adapters.token_adapter.adapter import TokenAdapter\n\n# Initialize adapter (no config needed)\nadapter = TokenAdapter()\n\n# Handle API errors\nsuccess, data = await adapter.get_token(\"invalid-address\")\nif not success:\n print(f\"API error: {data}\")\nelse:\n print(f\"Token found: {data}\")"
|
|
13
13
|
},
|
|
14
|
-
"health_check": {
|
|
15
|
-
"description": "Check adapter health and connectivity",
|
|
16
|
-
"code": "from adapters.token_adapter.adapter import TokenAdapter\n\n# Initialize adapter (no config needed)\nadapter = TokenAdapter()\n\n# Check health\nhealth = await adapter.health_check()\nprint(f\"Adapter status: {health['status']}\")\nprint(f\"Connected: {health['connected']}\")\nprint(f\"Adapter type: {health['adapter']}\")\n\nif health['status'] == 'healthy':\n print(\"Adapter is ready to use\")\nelse:\n print(f\"Adapter has issues: {health.get('error', 'Unknown error')}\")"
|
|
17
|
-
},
|
|
18
14
|
"batch_operations": {
|
|
19
15
|
"description": "Perform multiple token lookups efficiently",
|
|
20
16
|
"code": "from adapters.token_adapter.adapter import TokenAdapter\n\n# Initialize adapter (no config needed)\nadapter = TokenAdapter()\n\n# List of token addresses to lookup (Base chain)\ntoken_addresses = [\n \"0x1234567890abcdef1234567890abcdef12345678\",\n \"0xabcdef1234567890abcdef1234567890abcdef12\",\n \"0x9876543210fedcba9876543210fedcba98765432\"\n]\n\n# Batch lookup\ntoken_data = {}\nfor address in token_addresses:\n success, data = await adapter.get_token(address, chain_id=8453)\n if success:\n token_data[address] = data\n else:\n print(f\"Failed to get token for {address}: {data}\")\n\nprint(f\"Successfully retrieved {len(token_data)} tokens\")\nfor address, data in token_data.items():\n print(f\"{address}: {data.get('symbol', 'Unknown')} - {data.get('name', 'Unknown')}\")"
|