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.

Files changed (156) hide show
  1. wayfinder_paths/__init__.py +0 -4
  2. wayfinder_paths/adapters/balance_adapter/README.md +0 -1
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
  4. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  5. wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
  6. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  7. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  8. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  9. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  10. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  11. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  12. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  13. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  14. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  15. wayfinder_paths/adapters/brap_adapter/README.md +22 -75
  16. wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
  17. wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
  18. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  19. wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
  20. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
  21. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  22. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
  23. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  24. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
  25. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  26. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  28. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  29. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  30. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  31. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  32. wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
  33. wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
  34. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  35. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  36. wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
  37. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  38. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
  39. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  40. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  41. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  42. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  43. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  44. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  45. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  46. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  47. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  48. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  49. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  50. wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
  51. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  52. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  53. wayfinder_paths/conftest.py +24 -17
  54. wayfinder_paths/core/__init__.py +0 -3
  55. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  56. wayfinder_paths/core/adapters/models.py +17 -7
  57. wayfinder_paths/core/clients/BRAPClient.py +4 -1
  58. wayfinder_paths/core/clients/ClientManager.py +0 -7
  59. wayfinder_paths/core/clients/LedgerClient.py +196 -172
  60. wayfinder_paths/core/clients/TokenClient.py +47 -1
  61. wayfinder_paths/core/clients/WayfinderClient.py +1 -3
  62. wayfinder_paths/core/clients/__init__.py +0 -5
  63. wayfinder_paths/core/clients/protocols.py +21 -35
  64. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  65. wayfinder_paths/core/config.py +10 -162
  66. wayfinder_paths/core/constants/__init__.py +73 -2
  67. wayfinder_paths/core/constants/base.py +8 -17
  68. wayfinder_paths/core/constants/chains.py +36 -0
  69. wayfinder_paths/core/constants/contracts.py +52 -0
  70. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  71. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  72. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  73. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  74. wayfinder_paths/core/constants/tokens.py +9 -0
  75. wayfinder_paths/core/engine/manifest.py +66 -0
  76. wayfinder_paths/core/strategies/Strategy.py +0 -71
  77. wayfinder_paths/core/strategies/__init__.py +10 -1
  78. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  79. wayfinder_paths/core/utils/evm_helpers.py +5 -15
  80. wayfinder_paths/core/utils/test_transaction.py +289 -0
  81. wayfinder_paths/core/utils/tokens.py +28 -0
  82. wayfinder_paths/core/utils/transaction.py +57 -8
  83. wayfinder_paths/core/utils/web3.py +8 -3
  84. wayfinder_paths/mcp/__init__.py +5 -0
  85. wayfinder_paths/mcp/preview.py +185 -0
  86. wayfinder_paths/mcp/scripting.py +84 -0
  87. wayfinder_paths/mcp/server.py +52 -0
  88. wayfinder_paths/mcp/state/profile_store.py +195 -0
  89. wayfinder_paths/mcp/state/store.py +89 -0
  90. wayfinder_paths/mcp/test_scripting.py +267 -0
  91. wayfinder_paths/mcp/tools/__init__.py +0 -0
  92. wayfinder_paths/mcp/tools/balances.py +290 -0
  93. wayfinder_paths/mcp/tools/discovery.py +158 -0
  94. wayfinder_paths/mcp/tools/execute.py +770 -0
  95. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  96. wayfinder_paths/mcp/tools/quotes.py +288 -0
  97. wayfinder_paths/mcp/tools/run_script.py +286 -0
  98. wayfinder_paths/mcp/tools/strategies.py +188 -0
  99. wayfinder_paths/mcp/tools/tokens.py +46 -0
  100. wayfinder_paths/mcp/tools/wallets.py +354 -0
  101. wayfinder_paths/mcp/utils.py +129 -0
  102. wayfinder_paths/policies/enso.py +1 -2
  103. wayfinder_paths/policies/hyper_evm.py +6 -3
  104. wayfinder_paths/policies/hyperlend.py +1 -2
  105. wayfinder_paths/policies/hyperliquid.py +1 -1
  106. wayfinder_paths/policies/lifi.py +18 -0
  107. wayfinder_paths/policies/moonwell.py +12 -7
  108. wayfinder_paths/policies/prjx.py +1 -3
  109. wayfinder_paths/policies/util.py +8 -2
  110. wayfinder_paths/run_strategy.py +97 -300
  111. wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
  112. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
  113. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  114. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  115. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  116. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  117. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  118. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  119. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  120. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  121. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  122. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  123. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  124. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  125. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  126. wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
  127. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  128. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  129. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
  130. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
  131. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
  132. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  133. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
  134. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
  135. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  136. wayfinder_paths/tests/test_test_coverage.py +1 -4
  137. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  138. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  139. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  140. wayfinder_paths/core/clients/WalletClient.py +0 -41
  141. wayfinder_paths/core/engine/StrategyJob.py +0 -110
  142. wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
  143. wayfinder_paths/scripts/create_strategy.py +0 -139
  144. wayfinder_paths/scripts/make_wallets.py +0 -142
  145. wayfinder_paths/templates/adapter/README.md +0 -150
  146. wayfinder_paths/templates/adapter/adapter.py +0 -16
  147. wayfinder_paths/templates/adapter/examples.json +0 -8
  148. wayfinder_paths/templates/adapter/test_adapter.py +0 -30
  149. wayfinder_paths/templates/strategy/README.md +0 -186
  150. wayfinder_paths/templates/strategy/examples.json +0 -11
  151. wayfinder_paths/templates/strategy/strategy.py +0 -35
  152. wayfinder_paths/tests/test_smoke_manifest.py +0 -63
  153. wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
  154. wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
  155. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  156. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +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}")