wayfinder-paths 0.1.22__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/__init__.py +0 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
- 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/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -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/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- 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 +649 -547
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
- 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/adapter.py +14 -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/__init__.py +0 -3
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +4 -1
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -3
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +21 -35
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +10 -162
- wayfinder_paths/core/constants/__init__.py +73 -2
- wayfinder_paths/core/constants/base.py +8 -17
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +52 -0
- 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/constants/tokens.py +9 -0
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -71
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +57 -8
- wayfinder_paths/core/utils/web3.py +8 -3
- 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/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
- 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/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
- 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 +15 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
- 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.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
- wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from hexbytes import HexBytes
|
|
8
|
+
|
|
9
|
+
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
10
|
+
from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
|
|
11
|
+
|
|
12
|
+
MULTICALL3_DEFAULT_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"
|
|
13
|
+
MULTICALL3_ABI = [
|
|
14
|
+
{
|
|
15
|
+
"inputs": [
|
|
16
|
+
{
|
|
17
|
+
"components": [
|
|
18
|
+
{"internalType": "address", "name": "target", "type": "address"},
|
|
19
|
+
{"internalType": "bytes", "name": "callData", "type": "bytes"},
|
|
20
|
+
],
|
|
21
|
+
"internalType": "struct Multicall3.Call[]",
|
|
22
|
+
"name": "calls",
|
|
23
|
+
"type": "tuple[]",
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"name": "aggregate",
|
|
27
|
+
"outputs": [
|
|
28
|
+
{"internalType": "uint256", "name": "blockNumber", "type": "uint256"},
|
|
29
|
+
{"internalType": "bytes[]", "name": "returnData", "type": "bytes[]"},
|
|
30
|
+
],
|
|
31
|
+
"stateMutability": "payable",
|
|
32
|
+
"type": "function",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"inputs": [{"internalType": "address", "name": "addr", "type": "address"}],
|
|
36
|
+
"name": "getEthBalance",
|
|
37
|
+
"outputs": [
|
|
38
|
+
{"internalType": "uint256", "name": "balance", "type": "uint256"},
|
|
39
|
+
],
|
|
40
|
+
"stateMutability": "view",
|
|
41
|
+
"type": "function",
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class MulticallCall:
|
|
48
|
+
target: str
|
|
49
|
+
call_data: bytes | str
|
|
50
|
+
|
|
51
|
+
def as_tuple(self) -> tuple[str, bytes | str]:
|
|
52
|
+
return self.target, self.call_data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class MulticallResult:
|
|
57
|
+
block_number: int
|
|
58
|
+
return_data: Sequence[bytes]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MulticallAdapter(BaseAdapter):
|
|
62
|
+
"""Thin wrapper around Multicall3 for reusable multicall aggregates."""
|
|
63
|
+
|
|
64
|
+
adapter_type = "MULTICALL"
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
config: dict[str, Any] | None = None,
|
|
69
|
+
*,
|
|
70
|
+
chain_id: int | None = None,
|
|
71
|
+
web3: Any | None = None,
|
|
72
|
+
address: str | None = None,
|
|
73
|
+
abi: list[dict[str, Any]] | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
super().__init__("multicall_adapter", config)
|
|
76
|
+
|
|
77
|
+
if web3 is None:
|
|
78
|
+
raise ValueError("MulticallAdapter requires web3 instance")
|
|
79
|
+
self.chain_id = int(chain_id) if chain_id is not None else None
|
|
80
|
+
self.web3 = web3
|
|
81
|
+
|
|
82
|
+
checksum_address = self.web3.to_checksum_address(
|
|
83
|
+
address or MULTICALL3_DEFAULT_ADDRESS
|
|
84
|
+
)
|
|
85
|
+
self.contract = self.web3.eth.contract(
|
|
86
|
+
address=checksum_address, abi=abi or MULTICALL3_ABI
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def aggregate(
|
|
90
|
+
self,
|
|
91
|
+
calls: Iterable[MulticallCall | tuple[str, bytes | str]],
|
|
92
|
+
*,
|
|
93
|
+
value: int = 0,
|
|
94
|
+
) -> MulticallResult:
|
|
95
|
+
calls_list = list(calls)
|
|
96
|
+
if not calls_list:
|
|
97
|
+
return MulticallResult(block_number=0, return_data=[])
|
|
98
|
+
|
|
99
|
+
encoded_calls: list[tuple[str, bytes]] = []
|
|
100
|
+
for call in calls_list:
|
|
101
|
+
target, calldata = self._coerce_call(call)
|
|
102
|
+
encoded_calls.append((target, calldata))
|
|
103
|
+
|
|
104
|
+
block_number, return_data = await self.contract.functions.aggregate(
|
|
105
|
+
encoded_calls
|
|
106
|
+
).call({"value": int(value)})
|
|
107
|
+
payload = tuple(self._ensure_bytes(r) for r in return_data)
|
|
108
|
+
return MulticallResult(block_number=int(block_number), return_data=payload)
|
|
109
|
+
|
|
110
|
+
def build_call(self, target: str, call_data: bytes | str) -> MulticallCall:
|
|
111
|
+
checksum = self.web3.to_checksum_address(target)
|
|
112
|
+
normalized = self._normalize_call_data(call_data)
|
|
113
|
+
return MulticallCall(target=checksum, call_data=normalized)
|
|
114
|
+
|
|
115
|
+
def encode_eth_balance(self, account: str) -> MulticallCall:
|
|
116
|
+
calldata = self.contract.encode_abi("getEthBalance", args=[account])
|
|
117
|
+
return self.build_call(self.contract.address, calldata)
|
|
118
|
+
|
|
119
|
+
def encode_erc20_balance(self, token: str, account: str) -> MulticallCall:
|
|
120
|
+
addr = self.web3.to_checksum_address(token)
|
|
121
|
+
erc20 = self.web3.eth.contract(address=addr, abi=ERC20_ABI)
|
|
122
|
+
calldata = erc20.encode_abi("balanceOf", args=[account])
|
|
123
|
+
return self.build_call(addr, calldata)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def decode_uint256(data: bytes | str) -> int:
|
|
127
|
+
raw = MulticallAdapter._normalize_call_data(data)
|
|
128
|
+
if len(raw) < 32:
|
|
129
|
+
raw = raw.rjust(32, b"\x00")
|
|
130
|
+
return int.from_bytes(raw[-32:], byteorder="big")
|
|
131
|
+
|
|
132
|
+
def _coerce_call(
|
|
133
|
+
self, call: MulticallCall | tuple[str, bytes | str]
|
|
134
|
+
) -> tuple[str, bytes]:
|
|
135
|
+
if isinstance(call, MulticallCall):
|
|
136
|
+
target = self.web3.to_checksum_address(call.target)
|
|
137
|
+
calldata = self._normalize_call_data(call.call_data)
|
|
138
|
+
else:
|
|
139
|
+
target_str, call_data = call
|
|
140
|
+
target = self.web3.to_checksum_address(target_str)
|
|
141
|
+
calldata = self._normalize_call_data(call_data)
|
|
142
|
+
return target, calldata
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _normalize_call_data(data: bytes | str) -> bytes:
|
|
146
|
+
if isinstance(data, bytes):
|
|
147
|
+
return data
|
|
148
|
+
if isinstance(data, HexBytes):
|
|
149
|
+
return bytes(data)
|
|
150
|
+
if isinstance(data, str):
|
|
151
|
+
if data.startswith("0x"):
|
|
152
|
+
return bytes.fromhex(data[2:])
|
|
153
|
+
return data.encode()
|
|
154
|
+
raise TypeError("Unsupported calldata type")
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _ensure_bytes(data: bytes | str | HexBytes) -> bytes:
|
|
158
|
+
if isinstance(data, bytes):
|
|
159
|
+
return data
|
|
160
|
+
if isinstance(data, HexBytes):
|
|
161
|
+
return bytes(data)
|
|
162
|
+
if isinstance(data, str):
|
|
163
|
+
if data.startswith("0x"):
|
|
164
|
+
return bytes.fromhex(data[2:])
|
|
165
|
+
return data.encode()
|
|
166
|
+
raise TypeError("Unexpected return data type from multicall")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from web3 import AsyncHTTPProvider, AsyncWeb3
|
|
5
|
+
|
|
6
|
+
from wayfinder_paths.adapters.multicall_adapter.adapter import (
|
|
7
|
+
MulticallAdapter,
|
|
8
|
+
MulticallCall,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _DummyAggregate:
|
|
13
|
+
def __init__(self, expected_value: int, result):
|
|
14
|
+
self._expected_value = expected_value
|
|
15
|
+
self._result = result
|
|
16
|
+
|
|
17
|
+
async def call(self, tx_params):
|
|
18
|
+
assert tx_params == {"value": self._expected_value}
|
|
19
|
+
return self._result
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _DummyFunctions:
|
|
23
|
+
def __init__(self, expected_calls, expected_value: int, result):
|
|
24
|
+
self._expected_calls = expected_calls
|
|
25
|
+
self._expected_value = expected_value
|
|
26
|
+
self._result = result
|
|
27
|
+
|
|
28
|
+
def aggregate(self, calls):
|
|
29
|
+
assert calls == self._expected_calls
|
|
30
|
+
return _DummyAggregate(self._expected_value, self._result)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _DummyContract:
|
|
34
|
+
def __init__(self, expected_calls, expected_value: int, result):
|
|
35
|
+
self.functions = _DummyFunctions(expected_calls, expected_value, result)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestMulticallAdapter:
|
|
39
|
+
def test_normalize_call_data_hex_str(self):
|
|
40
|
+
raw = MulticallAdapter._normalize_call_data("0x1234")
|
|
41
|
+
assert raw == b"\x12\x34"
|
|
42
|
+
|
|
43
|
+
def test_decode_uint256(self):
|
|
44
|
+
value = 123
|
|
45
|
+
encoded = value.to_bytes(32, "big")
|
|
46
|
+
assert MulticallAdapter.decode_uint256(encoded) == value
|
|
47
|
+
|
|
48
|
+
def test_encode_balance_calls_produce_bytes(self):
|
|
49
|
+
w3 = AsyncWeb3(AsyncHTTPProvider("http://localhost:8545"))
|
|
50
|
+
adapter = MulticallAdapter(web3=w3)
|
|
51
|
+
|
|
52
|
+
eth_call = adapter.encode_eth_balance(
|
|
53
|
+
"0x0000000000000000000000000000000000000002"
|
|
54
|
+
)
|
|
55
|
+
assert isinstance(eth_call, MulticallCall)
|
|
56
|
+
assert isinstance(eth_call.call_data, (bytes, bytearray))
|
|
57
|
+
assert len(eth_call.call_data) > 0
|
|
58
|
+
|
|
59
|
+
erc20_call = adapter.encode_erc20_balance(
|
|
60
|
+
"0x0000000000000000000000000000000000000001",
|
|
61
|
+
"0x0000000000000000000000000000000000000002",
|
|
62
|
+
)
|
|
63
|
+
assert isinstance(erc20_call, MulticallCall)
|
|
64
|
+
assert isinstance(erc20_call.call_data, (bytes, bytearray))
|
|
65
|
+
assert len(erc20_call.call_data) > 0
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_aggregate_coerces_calls_and_returns_bytes(self):
|
|
69
|
+
w3 = AsyncWeb3(AsyncHTTPProvider("http://localhost:8545"))
|
|
70
|
+
adapter = MulticallAdapter(web3=w3)
|
|
71
|
+
|
|
72
|
+
call = adapter.build_call(
|
|
73
|
+
"0x0000000000000000000000000000000000000001", "0x1234"
|
|
74
|
+
)
|
|
75
|
+
expected_calls = [
|
|
76
|
+
(w3.to_checksum_address(call.target), b"\x12\x34"),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
dummy_contract = _DummyContract(
|
|
80
|
+
expected_calls=expected_calls,
|
|
81
|
+
expected_value=7,
|
|
82
|
+
result=(123, ["0x" + ("00" * 32)]),
|
|
83
|
+
)
|
|
84
|
+
adapter.contract = dummy_contract
|
|
85
|
+
|
|
86
|
+
result = await adapter.aggregate([call], value=7)
|
|
87
|
+
assert result.block_number == 123
|
|
88
|
+
assert result.return_data == (b"\x00" * 32,)
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_aggregate_empty_returns_empty_result(self):
|
|
92
|
+
w3 = AsyncWeb3(AsyncHTTPProvider("http://localhost:8545"))
|
|
93
|
+
adapter = MulticallAdapter(web3=w3)
|
|
94
|
+
|
|
95
|
+
result = await adapter.aggregate([])
|
|
96
|
+
assert result.block_number == 0
|
|
97
|
+
assert list(result.return_data) == []
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Pendle Adapter
|
|
2
|
+
|
|
3
|
+
Adapter for Pendle API + Hosted SDK endpoints to support:
|
|
4
|
+
|
|
5
|
+
- Market discovery (PT/YT markets, APYs, liquidity/volume/expiry filtering)
|
|
6
|
+
- Historical metrics (per-market time series)
|
|
7
|
+
- Execution planning (swap quote → ready-to-send `tx` + required `tokenApprovals`)
|
|
8
|
+
|
|
9
|
+
## Capabilities
|
|
10
|
+
|
|
11
|
+
- `pendle.markets.read`: Fetch whitelisted markets (`/v1/markets/all`)
|
|
12
|
+
- `pendle.market.snapshot`: Fetch a market snapshot (`/v2/{chainId}/markets/{market}/data`)
|
|
13
|
+
- `pendle.market.history`: Fetch market historical data (`/v2/{chainId}/markets/{market}/historical-data`)
|
|
14
|
+
- `pendle.prices.ohlcv`: Fetch token OHLCV (`/v4/{chainId}/prices/{token}/ohlcv`)
|
|
15
|
+
- `pendle.prices.assets`: Fetch all asset prices (`/v1/prices/assets`)
|
|
16
|
+
- `pendle.swap.quote`: Build Hosted SDK swap payload (`/v2/sdk/{chainId}/markets/{market}/swap`)
|
|
17
|
+
- `pendle.swap.best_pt`: Select and quote “best” PT swap on a chain
|
|
18
|
+
- `pendle.convert.quote`: Universal Hosted SDK convert quote (`/v2/sdk/{chainId}/convert`)
|
|
19
|
+
- `pendle.convert.best_pt`: Select and quote “best” PT via convert endpoint
|
|
20
|
+
- `pendle.convert.execute`: Broadcast Hosted SDK convert tx (incl approvals)
|
|
21
|
+
- `pendle.positions.database`: Indexed positions snapshot (`/v1/dashboard/positions/database/{user}`; claimables cached)
|
|
22
|
+
- `pendle.limit_orders.*`: Limit order discovery + maker APIs (`/v1/limit-orders/...`)
|
|
23
|
+
- `pendle.deployments.read`: Load Pendle core deployments JSON (router/routerStatic/limitRouter)
|
|
24
|
+
- `pendle.router_static.rates`: Off-chain spot-rate sanity checks via RouterStatic contract
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
- `PENDLE_API_URL` (env var): defaults to `https://api-v2.pendle.finance/core`
|
|
29
|
+
- Optional config:
|
|
30
|
+
- `config["pendle_adapter"]["base_url"]`
|
|
31
|
+
- `config["pendle_adapter"]["timeout"]`
|
|
32
|
+
- `config["pendle_adapter"]["deployments_base_url"]` (defaults to Pendle’s public core deployments on GitHub)
|
|
33
|
+
- `config["pendle_adapter"]["max_retries"]`, `retry_backoff_seconds`
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### List active PT/YT markets (multi-chain)
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from adapters.pendle_adapter.adapter import PendleAdapter
|
|
41
|
+
|
|
42
|
+
adapter = PendleAdapter()
|
|
43
|
+
|
|
44
|
+
rows = await adapter.list_active_pt_yt_markets(
|
|
45
|
+
chains=["arbitrum", "base", "hyperevm"],
|
|
46
|
+
min_liquidity_usd=250_000,
|
|
47
|
+
min_volume_usd_24h=25_000,
|
|
48
|
+
min_days_to_expiry=7,
|
|
49
|
+
sort_by="fixed_apy",
|
|
50
|
+
descending=True,
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Build the best PT swap transaction (single chain)
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
best = await adapter.build_best_pt_swap_tx(
|
|
58
|
+
chain="arbitrum",
|
|
59
|
+
token_in="0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # example: USDC (Arbitrum)
|
|
60
|
+
amount_in=str(1000 * 10**6), # 1000 USDC, base units (6 decimals)
|
|
61
|
+
receiver="0xYourEOAHere",
|
|
62
|
+
slippage=0.01,
|
|
63
|
+
enable_aggregator=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if best["ok"]:
|
|
67
|
+
tx = best["tx"]
|
|
68
|
+
approvals = best["tokenApprovals"]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Build a universal convert transaction (token -> PT)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
convert = await adapter.sdk_convert_v2(
|
|
75
|
+
chain="arbitrum",
|
|
76
|
+
slippage=0.01,
|
|
77
|
+
receiver="0xYourEOAHere",
|
|
78
|
+
inputs=[{"token": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "amount": str(1 * 10**6)}], # 1 USDC
|
|
79
|
+
outputs=["0x97c1a4ae3e0da8009aff13e3e3ee7ea5ee4afe84"], # PT token address
|
|
80
|
+
enable_aggregator=True,
|
|
81
|
+
aggregators=["kyberswap"],
|
|
82
|
+
additional_data=["impliedApy", "effectiveApy", "priceImpact"],
|
|
83
|
+
)
|
|
84
|
+
plan = adapter.build_convert_plan(chain="arbitrum", convert_response=convert)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Execute a universal convert (handles approvals + broadcast)
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
ok, res = await adapter.execute_convert(
|
|
91
|
+
chain="arbitrum",
|
|
92
|
+
slippage=0.01,
|
|
93
|
+
inputs=[{"token": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "amount": str(1 * 10**6)}],
|
|
94
|
+
outputs=["0x97c1a4ae3e0da8009aff13e3e3ee7ea5ee4afe84"],
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Notes
|
|
99
|
+
|
|
100
|
+
- “Fixed APY” proxy is `details.impliedApy` from `/v1/markets/all`.
|
|
101
|
+
- `build_best_pt_swap_tx()` requests Hosted SDK `additionalData=impliedApy,effectiveApy` and prefers `effectiveApy` when present.
|
|
102
|
+
- All Pendle REST/SDK responses include a `rateLimit` field populated from headers (x-ratelimit-* and x-computing-unit) for CU-aware budgeting.
|