wayfinder-paths 0.1.28__py3-none-any.whl → 0.1.30__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 (36) hide show
  1. wayfinder_paths/adapters/boros_adapter/adapter.py +445 -14
  2. wayfinder_paths/adapters/boros_adapter/client.py +7 -5
  3. wayfinder_paths/adapters/boros_adapter/test_adapter.py +259 -19
  4. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +4 -14
  5. wayfinder_paths/adapters/hyperliquid_adapter/exchange.py +2 -2
  6. wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +2 -33
  7. wayfinder_paths/adapters/multicall_adapter/adapter.py +2 -4
  8. wayfinder_paths/core/clients/TokenClient.py +1 -1
  9. wayfinder_paths/core/constants/__init__.py +23 -1
  10. wayfinder_paths/core/constants/contracts.py +22 -0
  11. wayfinder_paths/core/constants/hype_oft_abi.py +151 -0
  12. wayfinder_paths/core/constants/hyperliquid.py +20 -3
  13. wayfinder_paths/core/engine/manifest.py +1 -1
  14. wayfinder_paths/core/strategies/Strategy.py +1 -2
  15. wayfinder_paths/mcp/scripting.py +2 -2
  16. wayfinder_paths/mcp/tools/discovery.py +3 -72
  17. wayfinder_paths/mcp/tools/execute.py +8 -4
  18. wayfinder_paths/mcp/tools/hyperliquid.py +1 -1
  19. wayfinder_paths/mcp/tools/quotes.py +11 -133
  20. wayfinder_paths/mcp/tools/wallets.py +4 -7
  21. wayfinder_paths/mcp/utils.py +0 -22
  22. wayfinder_paths/policies/lifi.py +5 -2
  23. wayfinder_paths/policies/moonwell.py +3 -1
  24. wayfinder_paths/policies/util.py +4 -2
  25. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +2 -4
  26. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +57 -201
  27. wayfinder_paths/strategies/boros_hype_strategy/constants.py +24 -168
  28. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +24 -63
  29. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2 -0
  30. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2 -0
  31. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -1
  32. {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/METADATA +1 -1
  33. {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/RECORD +35 -35
  34. wayfinder_paths/core/types.py +0 -19
  35. {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/LICENSE +0 -0
  36. {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/WHEEL +0 -0
@@ -0,0 +1,151 @@
1
+ """LayerZero OFT ABI subset used for bridging native HYPE from HyperEVM."""
2
+
3
+ HYPE_OFT_ABI = [
4
+ {
5
+ "inputs": [
6
+ {
7
+ "components": [
8
+ {"internalType": "uint32", "name": "dstEid", "type": "uint32"},
9
+ {"internalType": "bytes32", "name": "to", "type": "bytes32"},
10
+ {"internalType": "uint256", "name": "amountLD", "type": "uint256"},
11
+ {
12
+ "internalType": "uint256",
13
+ "name": "minAmountLD",
14
+ "type": "uint256",
15
+ },
16
+ {
17
+ "internalType": "bytes",
18
+ "name": "extraOptions",
19
+ "type": "bytes",
20
+ },
21
+ {"internalType": "bytes", "name": "composeMsg", "type": "bytes"},
22
+ {"internalType": "bytes", "name": "oftCmd", "type": "bytes"},
23
+ ],
24
+ "internalType": "struct SendParam",
25
+ "name": "_sendParam",
26
+ "type": "tuple",
27
+ },
28
+ {"internalType": "bool", "name": "_payInLzToken", "type": "bool"},
29
+ ],
30
+ "name": "quoteSend",
31
+ "outputs": [
32
+ {
33
+ "components": [
34
+ {"internalType": "uint256", "name": "nativeFee", "type": "uint256"},
35
+ {
36
+ "internalType": "uint256",
37
+ "name": "lzTokenFee",
38
+ "type": "uint256",
39
+ },
40
+ ],
41
+ "internalType": "struct MessagingFee",
42
+ "name": "",
43
+ "type": "tuple",
44
+ }
45
+ ],
46
+ "stateMutability": "view",
47
+ "type": "function",
48
+ },
49
+ {
50
+ "inputs": [
51
+ {
52
+ "components": [
53
+ {"internalType": "uint32", "name": "dstEid", "type": "uint32"},
54
+ {"internalType": "bytes32", "name": "to", "type": "bytes32"},
55
+ {"internalType": "uint256", "name": "amountLD", "type": "uint256"},
56
+ {
57
+ "internalType": "uint256",
58
+ "name": "minAmountLD",
59
+ "type": "uint256",
60
+ },
61
+ {
62
+ "internalType": "bytes",
63
+ "name": "extraOptions",
64
+ "type": "bytes",
65
+ },
66
+ {"internalType": "bytes", "name": "composeMsg", "type": "bytes"},
67
+ {"internalType": "bytes", "name": "oftCmd", "type": "bytes"},
68
+ ],
69
+ "internalType": "struct SendParam",
70
+ "name": "_sendParam",
71
+ "type": "tuple",
72
+ },
73
+ {
74
+ "components": [
75
+ {"internalType": "uint256", "name": "nativeFee", "type": "uint256"},
76
+ {
77
+ "internalType": "uint256",
78
+ "name": "lzTokenFee",
79
+ "type": "uint256",
80
+ },
81
+ ],
82
+ "internalType": "struct MessagingFee",
83
+ "name": "_fee",
84
+ "type": "tuple",
85
+ },
86
+ {"internalType": "address", "name": "_refundAddress", "type": "address"},
87
+ ],
88
+ "name": "send",
89
+ "outputs": [
90
+ {
91
+ "components": [
92
+ {"internalType": "bytes32", "name": "guid", "type": "bytes32"},
93
+ {"internalType": "uint64", "name": "nonce", "type": "uint64"},
94
+ {
95
+ "components": [
96
+ {
97
+ "internalType": "uint256",
98
+ "name": "nativeFee",
99
+ "type": "uint256",
100
+ },
101
+ {
102
+ "internalType": "uint256",
103
+ "name": "lzTokenFee",
104
+ "type": "uint256",
105
+ },
106
+ ],
107
+ "internalType": "struct MessagingFee",
108
+ "name": "fee",
109
+ "type": "tuple",
110
+ },
111
+ ],
112
+ "internalType": "struct MessagingReceipt",
113
+ "name": "",
114
+ "type": "tuple",
115
+ },
116
+ {
117
+ "components": [
118
+ {
119
+ "internalType": "uint256",
120
+ "name": "amountSentLD",
121
+ "type": "uint256",
122
+ },
123
+ {
124
+ "internalType": "uint256",
125
+ "name": "amountReceivedLD",
126
+ "type": "uint256",
127
+ },
128
+ ],
129
+ "internalType": "struct OFTReceipt",
130
+ "name": "",
131
+ "type": "tuple",
132
+ },
133
+ ],
134
+ "stateMutability": "payable",
135
+ "type": "function",
136
+ },
137
+ {
138
+ "inputs": [],
139
+ "name": "sharedDecimals",
140
+ "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}],
141
+ "stateMutability": "view",
142
+ "type": "function",
143
+ },
144
+ {
145
+ "inputs": [],
146
+ "name": "decimalConversionRate",
147
+ "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
148
+ "stateMutability": "view",
149
+ "type": "function",
150
+ },
151
+ ]
@@ -2,10 +2,27 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
- HYPERLIQUID_BRIDGE_ADDRESS: str = "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7"
6
- ARBITRUM_USDC_ADDRESS: str = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
5
+ from wayfinder_paths.core.constants.contracts import (
6
+ ARBITRUM_USDC as ARBITRUM_USDC_ADDRESS,
7
+ )
8
+ from wayfinder_paths.core.constants.contracts import (
9
+ HYPE_FEE_WALLET,
10
+ )
11
+ from wayfinder_paths.core.constants.contracts import (
12
+ HYPERLIQUID_BRIDGE as HYPERLIQUID_BRIDGE_ADDRESS,
13
+ )
14
+
15
+ # Re-export addresses for backwards compatibility
16
+ __all__ = [
17
+ "ARBITRUM_USDC_ADDRESS",
18
+ "ARBITRUM_USDC_TOKEN_ID",
19
+ "HYPE_FEE_WALLET",
20
+ "HYPERLIQUID_BRIDGE_ADDRESS",
21
+ "DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP",
22
+ "DEFAULT_HYPERLIQUID_BUILDER_FEE",
23
+ ]
24
+
7
25
  ARBITRUM_USDC_TOKEN_ID: str = "usd-coin-arbitrum"
8
- HYPE_FEE_WALLET: str = "0xaA1D89f333857eD78F8434CC4f896A9293EFE65c"
9
26
 
10
27
  # Tenths of a basis point: 30 -> 0.030% (3 bps)
11
28
  DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP: int = 30
@@ -19,7 +19,7 @@ class StrategyManifest(BaseModel):
19
19
  )
20
20
  name: str | None = Field(
21
21
  default=None,
22
- description="Unique name identifier for this strategy instance. Used to look up dedicated wallet in wallets.json by label.",
22
+ description="Unique name identifier for this strategy instance. Used to look up dedicated wallet in config.json by label.",
23
23
  )
24
24
  permissions: dict[str, Any] = Field(default_factory=dict)
25
25
  adapters: list[AdapterRequirement] = Field(default_factory=list)
@@ -8,7 +8,6 @@ from loguru import logger
8
8
 
9
9
  from wayfinder_paths.core.clients.TokenClient import TokenDetails
10
10
  from wayfinder_paths.core.strategies.descriptors import StratDescriptor
11
- from wayfinder_paths.core.types import HyperliquidSignCallback
12
11
 
13
12
 
14
13
  class StatusDict(TypedDict):
@@ -52,7 +51,7 @@ class Strategy(ABC):
52
51
  main_wallet_signing_callback: Callable[[dict], Awaitable[str]] | None = None,
53
52
  strategy_wallet_signing_callback: Callable[[dict], Awaitable[str]]
54
53
  | None = None,
55
- strategy_sign_typed_data: HyperliquidSignCallback | None = None,
54
+ strategy_sign_typed_data: Callable[[dict], Awaitable[str]] | None = None,
56
55
  ):
57
56
  self.ledger_adapter = None
58
57
  self.logger = logger.bind(strategy=self.__class__.__name__)
@@ -57,8 +57,8 @@ def get_adapter[T](
57
57
  wallet = find_wallet_by_label(wallet_label)
58
58
  if not wallet:
59
59
  raise ValueError(
60
- f"Wallet '{wallet_label}' not found in wallets.json. "
61
- "Run 'just create-wallets' or check WALLETS_PATH."
60
+ f"Wallet '{wallet_label}' not found in config.json. "
61
+ "Run 'just create-wallets'."
62
62
  )
63
63
 
64
64
  private_key = wallet.get("private_key") or wallet.get("private_key_hex")
@@ -6,12 +6,12 @@ from wayfinder_paths.mcp.utils import err, ok, read_text_excerpt, read_yaml, rep
6
6
 
7
7
 
8
8
  async def discover(
9
- kind: Literal["adapters", "strategies"],
9
+ kind: Literal["adapter", "strategy"],
10
10
  detail: Literal["summary", "full"] = "summary",
11
11
  ) -> dict[str, Any]:
12
12
  root = repo_root()
13
13
  base = (
14
- root / "wayfinder_paths" / ("adapters" if kind == "adapters" else "strategies")
14
+ root / "wayfinder_paths" / ("adapters" if kind == "adapter" else "strategies")
15
15
  )
16
16
  if not base.exists():
17
17
  return err("not_found", f"Directory not found: {base}")
@@ -26,7 +26,7 @@ async def discover(
26
26
  manifest = read_yaml(manifest_path)
27
27
  rel_manifest = str(manifest_path.relative_to(root))
28
28
 
29
- if kind == "adapters":
29
+ if kind == "adapter":
30
30
  item = {
31
31
  "name": child.name,
32
32
  "entrypoint": manifest.get("entrypoint"),
@@ -58,75 +58,11 @@ async def discover(
58
58
  return ok({"kind": kind, "items": items})
59
59
 
60
60
 
61
- def _default_claude_notes_for_adapter(name: str) -> dict[str, Any] | None:
62
- # Keep this deliberately small; the real docs live in .claude/skills.
63
- if name == "brap_adapter":
64
- return {
65
- "reads": [
66
- {
67
- "name": "quote_swap (via mcp__wayfinder__quote_swap)",
68
- "summary": "Get best BRAP route + fees; no on-chain effects.",
69
- }
70
- ],
71
- "writes": [
72
- {
73
- "name": "swap (via mcp__wayfinder__execute kind=swap)",
74
- "risk": "funds",
75
- "summary": "May submit ERC20 approvals and a swap/bridge tx.",
76
- }
77
- ],
78
- "gotchas": [
79
- "USDT-style tokens may require setting allowance to 0 before approve.",
80
- "Cross-chain swaps still broadcast a tx on the source chain.",
81
- ],
82
- }
83
- if name == "balance_adapter":
84
- return {
85
- "reads": [
86
- {
87
- "name": "balances (via mcp__wayfinder__balances)",
88
- "summary": "Read token/pool balances via Wayfinder API.",
89
- }
90
- ],
91
- "writes": [
92
- {
93
- "name": "send (via mcp__wayfinder__execute kind=send)",
94
- "risk": "funds",
95
- "summary": "Native/ERC20 sends from a local wallet.",
96
- }
97
- ],
98
- "gotchas": ["Prefer DRY_RUN=1 until ready to broadcast."],
99
- }
100
- if name == "hyperliquid_adapter":
101
- return {
102
- "reads": [
103
- {
104
- "name": "hyperliquid (via mcp__wayfinder__hyperliquid)",
105
- "summary": "User state + market reads (no signing).",
106
- }
107
- ],
108
- "writes": [
109
- {
110
- "name": "hyperliquid_execute (via mcp__wayfinder__hyperliquid_execute)",
111
- "risk": "funds",
112
- "summary": "Perp orders/leverage/withdraw with local signing (gated by review prompt).",
113
- }
114
- ],
115
- "gotchas": [
116
- "Perp coins are mapped to asset_id (<10000).",
117
- "Builder fees are attributed to 0xaA1D89f333857eD78F8434CC4f896A9293EFE65c and use tenths-of-bp units.",
118
- "Prefer DRY_RUN=1 until ready to execute.",
119
- ],
120
- }
121
- return None
122
-
123
-
124
61
  async def describe(
125
62
  kind: Literal["adapter", "strategy"],
126
63
  name: str,
127
64
  include_manifest: bool = True,
128
65
  include_readme_excerpt: bool = True,
129
- include_claude_notes: bool = True,
130
66
  ) -> dict[str, Any]:
131
67
  root = repo_root()
132
68
  base = (
@@ -150,9 +86,4 @@ async def describe(
150
86
  if readme:
151
87
  out["readme_excerpt"] = readme
152
88
 
153
- if include_claude_notes and kind == "adapter":
154
- notes = _default_claude_notes_for_adapter(name)
155
- if notes:
156
- out["claude_notes"] = notes
157
-
158
89
  return ok(out)
@@ -38,7 +38,7 @@ from wayfinder_paths.mcp.utils import (
38
38
 
39
39
  class ExecutionRequest(BaseModel):
40
40
  kind: Literal["swap", "send", "hyperliquid_deposit"]
41
- wallet_label: str = Field(..., description="wallets.json label (e.g. main)")
41
+ wallet_label: str = Field(..., description="config.json wallet label (e.g. main)")
42
42
 
43
43
  # Shared
44
44
  amount: str = Field(..., description="Human units as a string (e.g. '1000')")
@@ -257,8 +257,10 @@ def _compact_quote(
257
257
  """Create a compact summary of quote data, stripping verbose nested structures."""
258
258
  result: dict[str, Any] = {}
259
259
 
260
- # Extract provider list from all_quotes
261
- all_quotes = quote_data.get("quotes", {}).get("all_quotes", [])
260
+ # Extract provider list from quotes (API returns quotes as a list at top level)
261
+ all_quotes = quote_data.get("quotes", [])
262
+ if not isinstance(all_quotes, list):
263
+ all_quotes = []
262
264
  if isinstance(all_quotes, list):
263
265
  result["providers"] = list(
264
266
  {
@@ -439,7 +441,7 @@ async def execute(
439
441
  if not sender or not pk:
440
442
  response = err(
441
443
  "invalid_wallet",
442
- "Wallet must include address and private_key_hex in wallets.json (local dev only)",
444
+ "Wallet must include address and private_key_hex in config.json (local dev only)",
443
445
  {"wallet_label": req.wallet_label},
444
446
  )
445
447
  store.put(key, tool_input, response)
@@ -530,6 +532,8 @@ async def execute(
530
532
  swap_tx = dict(calldata)
531
533
  swap_tx["chainId"] = int(from_chain_id)
532
534
  swap_tx["from"] = to_checksum_address(sender)
535
+ if "value" in swap_tx:
536
+ swap_tx["value"] = int(swap_tx["value"])
533
537
 
534
538
  token_addr = from_token_addr
535
539
  spender = swap_tx.get("to")
@@ -382,7 +382,7 @@ async def hyperliquid_execute(
382
382
  if not sender or not pk:
383
383
  response = err(
384
384
  "invalid_wallet",
385
- "Wallet must include address and private_key_hex in wallets.json (local dev only)",
385
+ "Wallet must include address and private_key_hex in config.json (local dev only)",
386
386
  {"wallet_label": want},
387
387
  )
388
388
  store.put(key, tool_input, response)
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- import re
4
3
  from typing import Any
5
4
 
6
5
  from wayfinder_paths.core.clients.BRAPClient import BRAPClient
7
6
  from wayfinder_paths.core.clients.TokenClient import TokenClient
8
- from wayfinder_paths.core.constants.chains import CHAIN_CODE_TO_ID
7
+ from wayfinder_paths.mcp.tools.execute import (
8
+ _resolve_token_meta,
9
+ _select_token_chain,
10
+ )
9
11
  from wayfinder_paths.mcp.utils import (
10
12
  err,
11
13
  find_wallet_by_label,
@@ -15,129 +17,6 @@ from wayfinder_paths.mcp.utils import (
15
17
  )
16
18
 
17
19
 
18
- def _chain_id(token: dict[str, Any]) -> int | None:
19
- # Token payloads often include a database/internal `id` field; do not treat that as a chain id.
20
- if token.get("chain_id") is not None:
21
- try:
22
- return int(token.get("chain_id"))
23
- except (TypeError, ValueError):
24
- return None
25
-
26
- chain = token.get("chain") or {}
27
- if not isinstance(chain, dict):
28
- return None
29
-
30
- for key in ("chain_id", "chainId", "id"):
31
- if chain.get(key) is None:
32
- continue
33
- try:
34
- return int(chain.get(key))
35
- except (TypeError, ValueError):
36
- return None
37
-
38
- return None
39
-
40
-
41
- _SIMPLE_CHAIN_SUFFIX_RE = re.compile(r"^[a-z0-9]+\s+[a-z0-9-]+$", re.IGNORECASE)
42
- _ASSET_CHAIN_SPLIT_RE = re.compile(
43
- r"^(?P<asset>[a-z0-9]+)[- _](?P<chain>[a-z0-9-]+)$", re.IGNORECASE
44
- )
45
-
46
-
47
- def _normalize_token_query(query: str) -> str:
48
- q = " ".join(str(query).strip().split())
49
- if not q or "-" in q or "_" in q:
50
- return q
51
- if not _SIMPLE_CHAIN_SUFFIX_RE.match(q):
52
- return q
53
- asset, chain_code = q.rsplit(" ", 1)
54
- if chain_code.lower() in CHAIN_CODE_TO_ID:
55
- return f"{asset}-{chain_code}"
56
- return q
57
-
58
-
59
- def _is_eth_like_token(meta: dict[str, Any]) -> bool:
60
- asset_id = str(meta.get("asset_id") or "").lower()
61
- symbol = str(meta.get("symbol") or "").lower()
62
- return asset_id == "ethereum" or symbol == "eth"
63
-
64
-
65
- def _split_asset_chain(query: str) -> tuple[str, str] | None:
66
- q = str(query).strip()
67
- if not q:
68
- return None
69
- m = _ASSET_CHAIN_SPLIT_RE.match(q)
70
- if not m:
71
- return None
72
- return m.group("asset").lower(), m.group("chain").lower()
73
-
74
-
75
- async def _resolve_token_meta(
76
- token_client: TokenClient,
77
- *,
78
- query: str,
79
- ) -> tuple[str, dict[str, Any]]:
80
- q = _normalize_token_query(query)
81
- split = _split_asset_chain(q)
82
- if split:
83
- asset, chain_code = split
84
- if asset in {"eth", "ethereum"}:
85
- try:
86
- gas_meta = await token_client.get_gas_token(chain_code)
87
- if isinstance(gas_meta, dict) and _is_eth_like_token(gas_meta):
88
- return q, gas_meta
89
- except Exception:
90
- pass
91
- meta = await token_client.get_token_details(q)
92
- return q, meta
93
-
94
-
95
- def _infer_chain_code_from_query(query: str, meta: dict[str, Any]) -> str | None:
96
- q = str(query).strip().lower()
97
- if not q:
98
- return None
99
-
100
- candidates: set[str] = {str(k).lower() for k in CHAIN_CODE_TO_ID.keys()}
101
- addrs = meta.get("addresses") or {}
102
- if isinstance(addrs, dict):
103
- candidates.update(str(k).lower() for k in addrs.keys())
104
-
105
- best: str | None = None
106
- for code in candidates:
107
- if q.endswith(f"-{code}"):
108
- if best is None or len(code) > len(best):
109
- best = code
110
- return best
111
-
112
-
113
- def _address_for_chain(meta: dict[str, Any], chain_code: str) -> str | None:
114
- addrs = meta.get("addresses") or {}
115
- if not isinstance(addrs, dict):
116
- return None
117
- for key, val in addrs.items():
118
- if str(key).lower() == chain_code and val:
119
- return str(val)
120
- return None
121
-
122
-
123
- def _select_token_chain(
124
- meta: dict[str, Any], *, query: str
125
- ) -> tuple[int | None, str | None]:
126
- chain_id = _chain_id(meta)
127
- token_address = meta.get("address")
128
-
129
- desired_chain = _infer_chain_code_from_query(query, meta)
130
- if desired_chain:
131
- addr = _address_for_chain(meta, desired_chain)
132
- if addr:
133
- token_address = addr
134
- if desired_chain in CHAIN_CODE_TO_ID:
135
- chain_id = CHAIN_CODE_TO_ID[desired_chain]
136
-
137
- token_address_out = str(token_address).strip() if token_address else None
138
- return chain_id, token_address_out
139
-
140
-
141
20
  def _slippage_float(slippage_bps: int) -> float:
142
21
  return max(0.0, float(int(slippage_bps)) / 10_000.0)
143
22
 
@@ -205,14 +84,13 @@ async def quote_swap(
205
84
  except Exception as exc: # noqa: BLE001
206
85
  return err("quote_error", str(exc))
207
86
 
208
- quotes_data = data.get("quotes", {}) if isinstance(data, dict) else {}
209
- if not isinstance(quotes_data, dict):
210
- quotes_data = {}
211
- all_quotes = quotes_data.get("all_quotes", [])
212
- if not isinstance(all_quotes, list):
213
- all_quotes = []
214
- best_quote = quotes_data.get("best_quote")
215
- quote_count = quotes_data.get("quote_count", len(all_quotes))
87
+ # API returns {"quotes": [...], "best_quote": {...}} at top level
88
+ raw_quotes = data.get("quotes", []) if isinstance(data, dict) else []
89
+ if not isinstance(raw_quotes, list):
90
+ raw_quotes = []
91
+ all_quotes = raw_quotes
92
+ best_quote = data.get("best_quote") if isinstance(data, dict) else None
93
+ quote_count = len(all_quotes)
216
94
 
217
95
  providers: list[str] = []
218
96
  seen: set[str] = set()
@@ -14,7 +14,6 @@ from wayfinder_paths.mcp.utils import (
14
14
  normalize_address,
15
15
  ok,
16
16
  repo_root,
17
- wallets_path,
18
17
  )
19
18
 
20
19
  PROTOCOL_ADAPTERS: dict[str, dict[str, Any]] = {
@@ -146,9 +145,7 @@ async def wallets(
146
145
  parallel: bool = False,
147
146
  include_zero_positions: bool = False,
148
147
  ) -> dict[str, Any]:
149
- p = wallets_path()
150
148
  root = repo_root()
151
- rel = str(p.relative_to(root)) if p.is_absolute() and root in p.parents else str(p)
152
149
  store = WalletProfileStore.default()
153
150
 
154
151
  if action == "list":
@@ -164,7 +161,7 @@ async def wallets(
164
161
  else:
165
162
  view["protocols"] = []
166
163
  wallet_list.append(view)
167
- return ok({"wallets_path": rel, "wallets": wallet_list})
164
+ return ok({"config_path": "config.json", "wallets": wallet_list})
168
165
 
169
166
  if action == "create":
170
167
  existing = load_wallets()
@@ -178,7 +175,7 @@ async def wallets(
178
175
  if str(w.get("label", "")).strip() == want:
179
176
  return ok(
180
177
  {
181
- "wallets_path": rel,
178
+ "config_path": "config.json",
182
179
  "wallets": [_public_wallet_view(x) for x in existing],
183
180
  "created": _public_wallet_view(w),
184
181
  "note": "Wallet label already existed; returning existing wallet.",
@@ -187,12 +184,12 @@ async def wallets(
187
184
 
188
185
  w = make_random_wallet()
189
186
  w["label"] = want
190
- write_wallet_to_json(w, out_dir=p.parent, filename=p.name)
187
+ write_wallet_to_json(w, out_dir=root, filename="config.json")
191
188
 
192
189
  refreshed = load_wallets()
193
190
  return ok(
194
191
  {
195
- "wallets_path": rel,
192
+ "config_path": "config.json",
196
193
  "wallets": [_public_wallet_view(x) for x in refreshed],
197
194
  "created": _public_wallet_view(w),
198
195
  }
@@ -64,32 +64,10 @@ def load_config_json() -> dict[str, Any]:
64
64
  return parsed if isinstance(parsed, dict) else {}
65
65
 
66
66
 
67
- def wallets_path() -> Path:
68
- cfg = load_config_json()
69
- system = cfg.get("system") if isinstance(cfg.get("system"), dict) else {}
70
- candidate = (
71
- (system or {}).get("wallets_path")
72
- or os.getenv("WALLETS_PATH")
73
- or "wallets.json"
74
- )
75
- p = Path(candidate)
76
- if not p.is_absolute():
77
- p = repo_root() / p
78
- return p
79
-
80
-
81
67
  def load_wallets() -> list[dict[str, Any]]:
82
68
  cfg = load_config_json()
83
69
  if isinstance(cfg.get("wallets"), list):
84
70
  return [w for w in cfg["wallets"] if isinstance(w, dict)]
85
-
86
- p = wallets_path()
87
- if p.exists():
88
- parsed = load_json_file(p)
89
- if isinstance(parsed, list):
90
- return [w for w in parsed if isinstance(w, dict)]
91
- if isinstance(parsed, dict) and isinstance(parsed.get("wallets"), list):
92
- return [w for w in parsed["wallets"] if isinstance(w, dict)]
93
71
  return []
94
72
 
95
73
 
@@ -1,7 +1,10 @@
1
+ from wayfinder_paths.core.constants.contracts import (
2
+ LIFI_GENERIC,
3
+ LIFI_ROUTER_HYPEREVM,
4
+ )
1
5
  from wayfinder_paths.policies.util import allow_functions
2
6
 
3
- LIFI_ROUTERS: dict[int:str] = {999: "0x0a0758d937d1059c356D4714e57F5df0239bce1A"}
4
- LIFI_GENERIC = "0x31a9b1835864706Af10103b31Ea2b79bdb995F5F"
7
+ LIFI_ROUTERS: dict[int:str] = {999: LIFI_ROUTER_HYPEREVM}
5
8
 
6
9
 
7
10
  async def lifi_swap(chain_id):
@@ -5,6 +5,7 @@ from wayfinder_paths.core.constants.contracts import (
5
5
  MOONWELL_M_WETH,
6
6
  MOONWELL_M_WSTETH,
7
7
  )
8
+ from wayfinder_paths.core.constants.moonwell_abi import COMPTROLLER_ABI
8
9
  from wayfinder_paths.policies.util import allow_functions
9
10
 
10
11
  WETH = BASE_WETH
@@ -52,8 +53,9 @@ async def mwsteth_approve_or_mint_or_redeem():
52
53
 
53
54
  async def moonwell_comptroller_enter_markets_or_claim_rewards():
54
55
  return await allow_functions(
55
- policy_name="Allow Moonwell Comptroller Enter Markets or Claim Rewards",
56
+ policy_name="Allow lend and claimReward",
56
57
  abi_chain_id=8453,
57
58
  address=COMPTROLLER,
58
59
  function_names=["enterMarkets", "claimReward"],
60
+ manual_abi=COMPTROLLER_ABI,
59
61
  )