wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.25__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 +2 -0
- 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/__init__.py +2 -0
- 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.25.dist-info/METADATA +377 -0
- wayfinder_paths-0.1.25.dist-info/RECORD +185 -0
- 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.25.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import importlib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from wayfinder_paths.core.config import CONFIG
|
|
9
|
+
from wayfinder_paths.core.engine.manifest import load_strategy_manifest
|
|
10
|
+
from wayfinder_paths.core.strategies.Strategy import Strategy
|
|
11
|
+
from wayfinder_paths.core.utils.evm_helpers import resolve_private_key_for_from_address
|
|
12
|
+
from wayfinder_paths.core.utils.web3 import get_transaction_chain_id, web3_from_chain_id
|
|
13
|
+
from wayfinder_paths.mcp.utils import err, ok, repo_root
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _strategy_dir(name: str) -> Path:
|
|
17
|
+
return repo_root() / "wayfinder_paths" / "strategies" / name
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_strategy_class(strategy_name: str) -> type[Strategy]:
|
|
21
|
+
manifest_path = _strategy_dir(strategy_name) / "manifest.yaml"
|
|
22
|
+
if not manifest_path.exists():
|
|
23
|
+
raise FileNotFoundError(f"Missing manifest.yaml for strategy: {strategy_name}")
|
|
24
|
+
manifest = load_strategy_manifest(str(manifest_path))
|
|
25
|
+
module_path, class_name = manifest.entrypoint.rsplit(".", 1)
|
|
26
|
+
module = importlib.import_module(module_path)
|
|
27
|
+
return getattr(module, class_name)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_strategy_config(strategy_name: str) -> dict[str, Any]:
|
|
31
|
+
config = dict(CONFIG.get("strategy", {}))
|
|
32
|
+
wallets = {w["label"]: w for w in CONFIG.get("wallets", [])}
|
|
33
|
+
|
|
34
|
+
if "main_wallet" not in config and "main" in wallets:
|
|
35
|
+
config["main_wallet"] = {"address": wallets["main"]["address"]}
|
|
36
|
+
if "strategy_wallet" not in config and strategy_name in wallets:
|
|
37
|
+
config["strategy_wallet"] = {"address": wallets[strategy_name]["address"]}
|
|
38
|
+
|
|
39
|
+
by_addr = {w["address"].lower(): w for w in CONFIG.get("wallets", [])}
|
|
40
|
+
for key in ("main_wallet", "strategy_wallet"):
|
|
41
|
+
if wallet := config.get(key):
|
|
42
|
+
if entry := by_addr.get(wallet.get("address", "").lower()):
|
|
43
|
+
if pk := entry.get("private_key") or entry.get("private_key_hex"):
|
|
44
|
+
wallet["private_key_hex"] = pk
|
|
45
|
+
return config
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _create_signing_callback(address: str, config: dict[str, Any]):
|
|
49
|
+
async def sign(transaction: dict) -> str:
|
|
50
|
+
pk = resolve_private_key_for_from_address(address, config)
|
|
51
|
+
async with web3_from_chain_id(get_transaction_chain_id(transaction)) as web3:
|
|
52
|
+
return web3.eth.account.sign_transaction(
|
|
53
|
+
transaction, pk
|
|
54
|
+
).raw_transaction.hex()
|
|
55
|
+
|
|
56
|
+
return sign
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def run_strategy(
|
|
60
|
+
*,
|
|
61
|
+
strategy: str,
|
|
62
|
+
action: Literal[
|
|
63
|
+
"status",
|
|
64
|
+
"analyze",
|
|
65
|
+
"snapshot",
|
|
66
|
+
"policy",
|
|
67
|
+
"deposit",
|
|
68
|
+
"update",
|
|
69
|
+
"withdraw",
|
|
70
|
+
"exit",
|
|
71
|
+
],
|
|
72
|
+
amount_usdc: float = 1000.0,
|
|
73
|
+
amount: float | None = None,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
if not strategy.strip():
|
|
76
|
+
return err("invalid_request", "strategy is required")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
strategy_class = _load_strategy_class(strategy)
|
|
80
|
+
except Exception as exc: # noqa: BLE001
|
|
81
|
+
return err("not_found", str(exc))
|
|
82
|
+
|
|
83
|
+
if action == "policy":
|
|
84
|
+
pol = getattr(strategy_class, "policies", None)
|
|
85
|
+
if not callable(pol):
|
|
86
|
+
return ok({"strategy": strategy, "action": action, "output": []})
|
|
87
|
+
try:
|
|
88
|
+
res = pol() # type: ignore[misc]
|
|
89
|
+
if asyncio.iscoroutine(res):
|
|
90
|
+
res = await res
|
|
91
|
+
return ok({"strategy": strategy, "action": action, "output": res})
|
|
92
|
+
except Exception as exc: # noqa: BLE001
|
|
93
|
+
return err("strategy_error", str(exc))
|
|
94
|
+
|
|
95
|
+
config = _get_strategy_config(strategy)
|
|
96
|
+
|
|
97
|
+
def signing_cb(key: str):
|
|
98
|
+
if addr := config.get(key, {}).get("address"):
|
|
99
|
+
return _create_signing_callback(addr, config)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
strategy_obj = strategy_class(
|
|
104
|
+
config,
|
|
105
|
+
main_wallet_signing_callback=signing_cb("main_wallet"),
|
|
106
|
+
strategy_wallet_signing_callback=signing_cb("strategy_wallet"),
|
|
107
|
+
)
|
|
108
|
+
except TypeError:
|
|
109
|
+
try:
|
|
110
|
+
strategy_obj = strategy_class(config=config)
|
|
111
|
+
except TypeError:
|
|
112
|
+
strategy_obj = strategy_class()
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
if hasattr(strategy_obj, "setup"):
|
|
116
|
+
await strategy_obj.setup()
|
|
117
|
+
|
|
118
|
+
if action == "status":
|
|
119
|
+
out = await strategy_obj.status()
|
|
120
|
+
return ok({"strategy": strategy, "action": action, "output": out})
|
|
121
|
+
|
|
122
|
+
if action == "analyze":
|
|
123
|
+
if hasattr(strategy_obj, "analyze"):
|
|
124
|
+
out = await strategy_obj.analyze(deposit_usdc=amount_usdc)
|
|
125
|
+
return ok({"strategy": strategy, "action": action, "output": out})
|
|
126
|
+
return err("not_supported", "Strategy does not support analyze()")
|
|
127
|
+
|
|
128
|
+
if action == "snapshot":
|
|
129
|
+
if hasattr(strategy_obj, "build_batch_snapshot"):
|
|
130
|
+
out = await strategy_obj.build_batch_snapshot(
|
|
131
|
+
score_deposit_usdc=amount_usdc
|
|
132
|
+
)
|
|
133
|
+
return ok({"strategy": strategy, "action": action, "output": out})
|
|
134
|
+
return err(
|
|
135
|
+
"not_supported", "Strategy does not support build_batch_snapshot()"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if action == "deposit":
|
|
139
|
+
if amount is None:
|
|
140
|
+
return err("invalid_request", "amount required for deposit")
|
|
141
|
+
success, msg = await strategy_obj.deposit(amount=amount)
|
|
142
|
+
return ok(
|
|
143
|
+
{
|
|
144
|
+
"strategy": strategy,
|
|
145
|
+
"action": action,
|
|
146
|
+
"success": success,
|
|
147
|
+
"message": msg,
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if action == "update":
|
|
152
|
+
success, msg = await strategy_obj.update()
|
|
153
|
+
return ok(
|
|
154
|
+
{
|
|
155
|
+
"strategy": strategy,
|
|
156
|
+
"action": action,
|
|
157
|
+
"success": success,
|
|
158
|
+
"message": msg,
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if action == "withdraw":
|
|
163
|
+
success, msg = await strategy_obj.withdraw(amount=amount)
|
|
164
|
+
return ok(
|
|
165
|
+
{
|
|
166
|
+
"strategy": strategy,
|
|
167
|
+
"action": action,
|
|
168
|
+
"success": success,
|
|
169
|
+
"message": msg,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if action == "exit":
|
|
174
|
+
if hasattr(strategy_obj, "exit"):
|
|
175
|
+
success, msg = await strategy_obj.exit()
|
|
176
|
+
return ok(
|
|
177
|
+
{
|
|
178
|
+
"strategy": strategy,
|
|
179
|
+
"action": action,
|
|
180
|
+
"success": success,
|
|
181
|
+
"message": msg,
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
return err("not_supported", "Strategy does not support exit()")
|
|
185
|
+
|
|
186
|
+
return err("invalid_request", f"Unknown action: {action}")
|
|
187
|
+
except Exception as exc: # noqa: BLE001
|
|
188
|
+
return err("strategy_error", str(exc))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
6
|
+
from wayfinder_paths.mcp.utils import err, ok
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def tokens(
|
|
10
|
+
action: Literal["resolve", "gas", "fuzzy"],
|
|
11
|
+
query: str | None = None,
|
|
12
|
+
chain_code: str | None = None,
|
|
13
|
+
) -> dict[str, Any]:
|
|
14
|
+
client = TokenClient()
|
|
15
|
+
try:
|
|
16
|
+
if action == "resolve":
|
|
17
|
+
q = (query or "").strip()
|
|
18
|
+
if not q:
|
|
19
|
+
return err(
|
|
20
|
+
"invalid_request", "query is required for tokens(action=resolve)"
|
|
21
|
+
)
|
|
22
|
+
token = await client.get_token_details(q)
|
|
23
|
+
return ok({"token": token})
|
|
24
|
+
|
|
25
|
+
if action == "gas":
|
|
26
|
+
cc = (chain_code or "").strip()
|
|
27
|
+
if not cc:
|
|
28
|
+
return err(
|
|
29
|
+
"invalid_request", "chain_code is required for tokens(action=gas)"
|
|
30
|
+
)
|
|
31
|
+
token = await client.get_gas_token(cc)
|
|
32
|
+
return ok({"token": token})
|
|
33
|
+
|
|
34
|
+
if action == "fuzzy":
|
|
35
|
+
q = (query or "").strip()
|
|
36
|
+
if not q:
|
|
37
|
+
return err(
|
|
38
|
+
"invalid_request", "query is required for tokens(action=fuzzy)"
|
|
39
|
+
)
|
|
40
|
+
cc = (chain_code or "").strip() or None
|
|
41
|
+
result = await client.fuzzy_search(q, chain=cc)
|
|
42
|
+
return ok(result)
|
|
43
|
+
|
|
44
|
+
return err("invalid_request", f"Unknown tokens action: {action}")
|
|
45
|
+
except Exception as exc: # noqa: BLE001
|
|
46
|
+
return err("token_error", str(exc))
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import importlib
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from wayfinder_paths.core.utils.wallets import make_random_wallet, write_wallet_to_json
|
|
9
|
+
from wayfinder_paths.mcp.state.profile_store import WalletProfileStore
|
|
10
|
+
from wayfinder_paths.mcp.utils import (
|
|
11
|
+
err,
|
|
12
|
+
find_wallet_by_label,
|
|
13
|
+
load_wallets,
|
|
14
|
+
normalize_address,
|
|
15
|
+
ok,
|
|
16
|
+
repo_root,
|
|
17
|
+
wallets_path,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
PROTOCOL_ADAPTERS: dict[str, dict[str, Any]] = {
|
|
21
|
+
"hyperliquid": {
|
|
22
|
+
"module": "wayfinder_paths.adapters.hyperliquid_adapter.adapter",
|
|
23
|
+
"class": "HyperliquidAdapter",
|
|
24
|
+
"init_kwargs": {"simulation": True},
|
|
25
|
+
"method": "get_full_user_state",
|
|
26
|
+
"account_param": "account",
|
|
27
|
+
"extra_kwargs": {},
|
|
28
|
+
},
|
|
29
|
+
"hyperlend": {
|
|
30
|
+
"module": "wayfinder_paths.adapters.hyperlend_adapter.adapter",
|
|
31
|
+
"class": "HyperlendAdapter",
|
|
32
|
+
"init_kwargs": {},
|
|
33
|
+
"method": "get_full_user_state",
|
|
34
|
+
"account_param": "account",
|
|
35
|
+
"extra_kwargs": {"include_zero_positions": False},
|
|
36
|
+
},
|
|
37
|
+
"moonwell": {
|
|
38
|
+
"module": "wayfinder_paths.adapters.moonwell_adapter.adapter",
|
|
39
|
+
"class": "MoonwellAdapter",
|
|
40
|
+
"init_kwargs": {},
|
|
41
|
+
"method": "get_full_user_state",
|
|
42
|
+
"account_param": "account",
|
|
43
|
+
"extra_kwargs": {"include_zero_positions": False},
|
|
44
|
+
},
|
|
45
|
+
"boros": {
|
|
46
|
+
"module": "wayfinder_paths.adapters.boros_adapter.adapter",
|
|
47
|
+
"class": "BorosAdapter",
|
|
48
|
+
"init_kwargs": {},
|
|
49
|
+
"method": "get_full_user_state",
|
|
50
|
+
"account_param": "account",
|
|
51
|
+
"extra_kwargs": {},
|
|
52
|
+
},
|
|
53
|
+
"pendle": {
|
|
54
|
+
"module": "wayfinder_paths.adapters.pendle_adapter.adapter",
|
|
55
|
+
"class": "PendleAdapter",
|
|
56
|
+
"init_kwargs": {},
|
|
57
|
+
"method": "get_full_user_state",
|
|
58
|
+
"account_param": "account",
|
|
59
|
+
"extra_kwargs": {"chain": 42161, "include_zero_positions": False},
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _public_wallet_view(w: dict[str, Any]) -> dict[str, Any]:
|
|
65
|
+
return {"label": w.get("label"), "address": w.get("address")}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolve_wallet_address(
|
|
69
|
+
*, wallet_label: str | None, wallet_address: str | None
|
|
70
|
+
) -> tuple[str | None, str | None]:
|
|
71
|
+
waddr = normalize_address(wallet_address)
|
|
72
|
+
if waddr:
|
|
73
|
+
return waddr, None
|
|
74
|
+
|
|
75
|
+
want = (wallet_label or "").strip()
|
|
76
|
+
if not want:
|
|
77
|
+
return None, None
|
|
78
|
+
|
|
79
|
+
w = find_wallet_by_label(want)
|
|
80
|
+
if not w:
|
|
81
|
+
return None, None
|
|
82
|
+
|
|
83
|
+
return normalize_address(w.get("address")), want
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def _query_adapter(
|
|
87
|
+
protocol: str,
|
|
88
|
+
address: str,
|
|
89
|
+
include_zero_positions: bool = False,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
config = PROTOCOL_ADAPTERS.get(protocol)
|
|
92
|
+
if not config:
|
|
93
|
+
return {
|
|
94
|
+
"protocol": protocol,
|
|
95
|
+
"ok": False,
|
|
96
|
+
"error": f"Unknown protocol: {protocol}",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
start = time.time()
|
|
100
|
+
try:
|
|
101
|
+
module = importlib.import_module(config["module"])
|
|
102
|
+
adapter_class = getattr(module, config["class"])
|
|
103
|
+
adapter = adapter_class(**config["init_kwargs"])
|
|
104
|
+
|
|
105
|
+
method = getattr(adapter, config["method"])
|
|
106
|
+
kwargs = {config["account_param"]: address, **config["extra_kwargs"]}
|
|
107
|
+
|
|
108
|
+
if "include_zero_positions" in config["extra_kwargs"]:
|
|
109
|
+
kwargs["include_zero_positions"] = include_zero_positions
|
|
110
|
+
|
|
111
|
+
success, data = await method(**kwargs)
|
|
112
|
+
duration = time.time() - start
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"protocol": protocol,
|
|
116
|
+
"ok": bool(success),
|
|
117
|
+
"data": data if success else None,
|
|
118
|
+
"error": data if not success else None,
|
|
119
|
+
"duration_s": round(duration, 3),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
duration = time.time() - start
|
|
124
|
+
return {
|
|
125
|
+
"protocol": protocol,
|
|
126
|
+
"ok": False,
|
|
127
|
+
"error": str(exc),
|
|
128
|
+
"duration_s": round(duration, 3),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def wallets(
|
|
133
|
+
action: Literal["list", "create", "get", "annotate", "discover_portfolio"],
|
|
134
|
+
*,
|
|
135
|
+
label: str | None = None,
|
|
136
|
+
wallet_label: str | None = None,
|
|
137
|
+
wallet_address: str | None = None,
|
|
138
|
+
protocol: str | None = None,
|
|
139
|
+
annotate_action: str | None = None,
|
|
140
|
+
tool: str | None = None,
|
|
141
|
+
status: str | None = None,
|
|
142
|
+
chain_id: int | None = None,
|
|
143
|
+
details: dict[str, Any] | None = None,
|
|
144
|
+
idempotency_key: str | None = None,
|
|
145
|
+
protocols: list[str] | None = None,
|
|
146
|
+
parallel: bool = False,
|
|
147
|
+
include_zero_positions: bool = False,
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
p = wallets_path()
|
|
150
|
+
root = repo_root()
|
|
151
|
+
rel = str(p.relative_to(root)) if p.is_absolute() and root in p.parents else str(p)
|
|
152
|
+
store = WalletProfileStore.default()
|
|
153
|
+
|
|
154
|
+
if action == "list":
|
|
155
|
+
existing = load_wallets()
|
|
156
|
+
wallet_list = []
|
|
157
|
+
for w in existing:
|
|
158
|
+
view = _public_wallet_view(w)
|
|
159
|
+
# Enrich with tracked protocols (lowercase to match profile store)
|
|
160
|
+
addr = normalize_address(w.get("address"))
|
|
161
|
+
if addr:
|
|
162
|
+
tracked = store.get_protocols_for_wallet(addr.lower())
|
|
163
|
+
view["protocols"] = tracked
|
|
164
|
+
else:
|
|
165
|
+
view["protocols"] = []
|
|
166
|
+
wallet_list.append(view)
|
|
167
|
+
return ok({"wallets_path": rel, "wallets": wallet_list})
|
|
168
|
+
|
|
169
|
+
if action == "create":
|
|
170
|
+
existing = load_wallets()
|
|
171
|
+
want = (label or "").strip()
|
|
172
|
+
if not want:
|
|
173
|
+
return err(
|
|
174
|
+
"invalid_request", "label is required for wallets(action=create)"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
for w in existing:
|
|
178
|
+
if str(w.get("label", "")).strip() == want:
|
|
179
|
+
return ok(
|
|
180
|
+
{
|
|
181
|
+
"wallets_path": rel,
|
|
182
|
+
"wallets": [_public_wallet_view(x) for x in existing],
|
|
183
|
+
"created": _public_wallet_view(w),
|
|
184
|
+
"note": "Wallet label already existed; returning existing wallet.",
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
w = make_random_wallet()
|
|
189
|
+
w["label"] = want
|
|
190
|
+
write_wallet_to_json(w, out_dir=p.parent, filename=p.name)
|
|
191
|
+
|
|
192
|
+
refreshed = load_wallets()
|
|
193
|
+
return ok(
|
|
194
|
+
{
|
|
195
|
+
"wallets_path": rel,
|
|
196
|
+
"wallets": [_public_wallet_view(x) for x in refreshed],
|
|
197
|
+
"created": _public_wallet_view(w),
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if action == "get":
|
|
202
|
+
address, lbl = _resolve_wallet_address(
|
|
203
|
+
wallet_label=wallet_label or label, wallet_address=wallet_address
|
|
204
|
+
)
|
|
205
|
+
if not address:
|
|
206
|
+
return err(
|
|
207
|
+
"invalid_request",
|
|
208
|
+
"wallet_label or wallet_address is required",
|
|
209
|
+
{"wallet_label": wallet_label, "wallet_address": wallet_address},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
profile = store.get_profile(address)
|
|
213
|
+
return ok(
|
|
214
|
+
{
|
|
215
|
+
"action": "get",
|
|
216
|
+
"address": address,
|
|
217
|
+
"label": lbl,
|
|
218
|
+
"profile": profile,
|
|
219
|
+
"found": profile is not None,
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if action == "annotate":
|
|
224
|
+
address, lbl = _resolve_wallet_address(
|
|
225
|
+
wallet_label=wallet_label or label, wallet_address=wallet_address
|
|
226
|
+
)
|
|
227
|
+
if not address:
|
|
228
|
+
return err(
|
|
229
|
+
"invalid_request",
|
|
230
|
+
"wallet_label or wallet_address is required",
|
|
231
|
+
)
|
|
232
|
+
if not protocol:
|
|
233
|
+
return err("invalid_request", "protocol is required for annotate")
|
|
234
|
+
if not annotate_action:
|
|
235
|
+
return err("invalid_request", "annotate_action is required for annotate")
|
|
236
|
+
if not tool:
|
|
237
|
+
return err("invalid_request", "tool is required for annotate")
|
|
238
|
+
if not status:
|
|
239
|
+
return err("invalid_request", "status is required for annotate")
|
|
240
|
+
|
|
241
|
+
store.annotate(
|
|
242
|
+
address=address,
|
|
243
|
+
label=lbl,
|
|
244
|
+
protocol=protocol,
|
|
245
|
+
action=annotate_action,
|
|
246
|
+
tool=tool,
|
|
247
|
+
status=status,
|
|
248
|
+
chain_id=chain_id,
|
|
249
|
+
details=details,
|
|
250
|
+
idempotency_key=idempotency_key,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return ok(
|
|
254
|
+
{
|
|
255
|
+
"action": "annotate",
|
|
256
|
+
"address": address,
|
|
257
|
+
"protocol": protocol,
|
|
258
|
+
"annotated": True,
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if action == "discover_portfolio":
|
|
263
|
+
address, lbl = _resolve_wallet_address(
|
|
264
|
+
wallet_label=wallet_label or label, wallet_address=wallet_address
|
|
265
|
+
)
|
|
266
|
+
if not address:
|
|
267
|
+
return err(
|
|
268
|
+
"invalid_request",
|
|
269
|
+
"wallet_label or wallet_address is required for discover_portfolio",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
profile_protocols = store.get_protocols_for_wallet(address)
|
|
273
|
+
|
|
274
|
+
if protocols:
|
|
275
|
+
target_protocols = list(dict.fromkeys(protocols)) # dedupe, preserve order
|
|
276
|
+
else:
|
|
277
|
+
target_protocols = profile_protocols
|
|
278
|
+
|
|
279
|
+
supported_protocols = [p for p in target_protocols if p in PROTOCOL_ADAPTERS]
|
|
280
|
+
unsupported = [p for p in target_protocols if p not in PROTOCOL_ADAPTERS]
|
|
281
|
+
|
|
282
|
+
if not supported_protocols:
|
|
283
|
+
return ok(
|
|
284
|
+
{
|
|
285
|
+
"action": "discover_portfolio",
|
|
286
|
+
"address": address,
|
|
287
|
+
"label": lbl,
|
|
288
|
+
"profile_protocols": profile_protocols,
|
|
289
|
+
"positions": [],
|
|
290
|
+
"warning": "No supported protocols to query",
|
|
291
|
+
"unsupported_protocols": unsupported,
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if len(supported_protocols) >= 3 and not parallel:
|
|
296
|
+
return ok(
|
|
297
|
+
{
|
|
298
|
+
"action": "discover_portfolio",
|
|
299
|
+
"address": address,
|
|
300
|
+
"label": lbl,
|
|
301
|
+
"profile_protocols": profile_protocols,
|
|
302
|
+
"supported_protocols": supported_protocols,
|
|
303
|
+
"requires_confirmation": True,
|
|
304
|
+
"warning": f"Found {len(supported_protocols)} protocols to query. "
|
|
305
|
+
f"Set parallel=true for concurrent queries, or filter with protocols=[...] "
|
|
306
|
+
f"to query specific protocols.",
|
|
307
|
+
"protocols_to_query": supported_protocols,
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
start = time.time()
|
|
312
|
+
results: list[dict[str, Any]] = []
|
|
313
|
+
|
|
314
|
+
if parallel:
|
|
315
|
+
tasks = [
|
|
316
|
+
_query_adapter(proto, address, include_zero_positions)
|
|
317
|
+
for proto in supported_protocols
|
|
318
|
+
]
|
|
319
|
+
results = await asyncio.gather(*tasks)
|
|
320
|
+
else:
|
|
321
|
+
for proto in supported_protocols:
|
|
322
|
+
result = await _query_adapter(proto, address, include_zero_positions)
|
|
323
|
+
results.append(result)
|
|
324
|
+
|
|
325
|
+
total_duration = time.time() - start
|
|
326
|
+
all_positions: list[dict[str, Any]] = []
|
|
327
|
+
for r in results:
|
|
328
|
+
if r.get("ok") and r.get("data"):
|
|
329
|
+
data = r["data"]
|
|
330
|
+
positions = data.get("positions", [])
|
|
331
|
+
if positions:
|
|
332
|
+
for pos in positions:
|
|
333
|
+
all_positions.append(
|
|
334
|
+
{"protocol": r["protocol"], "position": pos}
|
|
335
|
+
)
|
|
336
|
+
r["data"] = data
|
|
337
|
+
|
|
338
|
+
return ok(
|
|
339
|
+
{
|
|
340
|
+
"action": "discover_portfolio",
|
|
341
|
+
"address": address,
|
|
342
|
+
"label": lbl,
|
|
343
|
+
"profile_protocols": profile_protocols,
|
|
344
|
+
"queried_protocols": supported_protocols,
|
|
345
|
+
"results": results,
|
|
346
|
+
"positions_count": len(all_positions),
|
|
347
|
+
"positions_summary": all_positions[:10],
|
|
348
|
+
"total_duration_s": round(total_duration, 3),
|
|
349
|
+
"parallel": parallel,
|
|
350
|
+
"unsupported_protocols": unsupported if unsupported else None,
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return err("invalid_request", f"Unknown action: {action}")
|