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.

Files changed (122) hide show
  1. wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
  2. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
  4. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  5. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  6. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  7. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  8. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  9. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  10. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  11. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  12. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  13. wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  15. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
  16. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
  18. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  19. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
  20. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  21. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  23. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  24. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  27. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  28. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  29. wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
  30. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  31. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
  32. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  33. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  34. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  35. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  36. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  37. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  38. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  39. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  40. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  41. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  42. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  43. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  44. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  45. wayfinder_paths/conftest.py +24 -17
  46. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  47. wayfinder_paths/core/adapters/models.py +17 -7
  48. wayfinder_paths/core/clients/BRAPClient.py +1 -1
  49. wayfinder_paths/core/clients/TokenClient.py +47 -1
  50. wayfinder_paths/core/clients/WayfinderClient.py +1 -2
  51. wayfinder_paths/core/clients/protocols.py +21 -22
  52. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  53. wayfinder_paths/core/config.py +12 -0
  54. wayfinder_paths/core/constants/__init__.py +15 -0
  55. wayfinder_paths/core/constants/base.py +6 -1
  56. wayfinder_paths/core/constants/contracts.py +39 -26
  57. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  58. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  59. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  60. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  61. wayfinder_paths/core/engine/manifest.py +66 -0
  62. wayfinder_paths/core/strategies/Strategy.py +0 -61
  63. wayfinder_paths/core/strategies/__init__.py +10 -1
  64. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  65. wayfinder_paths/core/utils/test_transaction.py +289 -0
  66. wayfinder_paths/core/utils/transaction.py +44 -1
  67. wayfinder_paths/core/utils/web3.py +3 -0
  68. wayfinder_paths/mcp/__init__.py +5 -0
  69. wayfinder_paths/mcp/preview.py +185 -0
  70. wayfinder_paths/mcp/scripting.py +84 -0
  71. wayfinder_paths/mcp/server.py +52 -0
  72. wayfinder_paths/mcp/state/profile_store.py +195 -0
  73. wayfinder_paths/mcp/state/store.py +89 -0
  74. wayfinder_paths/mcp/test_scripting.py +267 -0
  75. wayfinder_paths/mcp/tools/__init__.py +0 -0
  76. wayfinder_paths/mcp/tools/balances.py +290 -0
  77. wayfinder_paths/mcp/tools/discovery.py +158 -0
  78. wayfinder_paths/mcp/tools/execute.py +770 -0
  79. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  80. wayfinder_paths/mcp/tools/quotes.py +288 -0
  81. wayfinder_paths/mcp/tools/run_script.py +286 -0
  82. wayfinder_paths/mcp/tools/strategies.py +188 -0
  83. wayfinder_paths/mcp/tools/tokens.py +46 -0
  84. wayfinder_paths/mcp/tools/wallets.py +354 -0
  85. wayfinder_paths/mcp/utils.py +129 -0
  86. wayfinder_paths/policies/hyperliquid.py +1 -1
  87. wayfinder_paths/policies/lifi.py +18 -0
  88. wayfinder_paths/policies/util.py +8 -2
  89. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
  90. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  91. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  92. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  93. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  94. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  95. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  96. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  97. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  98. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  99. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  100. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  101. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  102. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  103. wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
  104. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  105. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  106. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
  107. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
  108. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
  109. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  110. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
  111. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
  112. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  113. wayfinder_paths/tests/test_test_coverage.py +1 -4
  114. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  115. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  116. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  117. wayfinder_paths/scripts/create_strategy.py +0 -139
  118. wayfinder_paths/scripts/make_wallets.py +0 -142
  119. wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
  120. wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
  121. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  122. {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
+ )
@@ -0,0 +1,6 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "adapters.pool_adapter.adapter.PoolAdapter"
3
+ capabilities:
4
+ - "pool.read"
5
+ - "pool.discover"
6
+ dependencies: []
@@ -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')}\")"