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.
- wayfinder_paths/adapters/boros_adapter/adapter.py +445 -14
- wayfinder_paths/adapters/boros_adapter/client.py +7 -5
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +259 -19
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +4 -14
- wayfinder_paths/adapters/hyperliquid_adapter/exchange.py +2 -2
- wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +2 -33
- wayfinder_paths/adapters/multicall_adapter/adapter.py +2 -4
- wayfinder_paths/core/clients/TokenClient.py +1 -1
- wayfinder_paths/core/constants/__init__.py +23 -1
- wayfinder_paths/core/constants/contracts.py +22 -0
- wayfinder_paths/core/constants/hype_oft_abi.py +151 -0
- wayfinder_paths/core/constants/hyperliquid.py +20 -3
- wayfinder_paths/core/engine/manifest.py +1 -1
- wayfinder_paths/core/strategies/Strategy.py +1 -2
- wayfinder_paths/mcp/scripting.py +2 -2
- wayfinder_paths/mcp/tools/discovery.py +3 -72
- wayfinder_paths/mcp/tools/execute.py +8 -4
- wayfinder_paths/mcp/tools/hyperliquid.py +1 -1
- wayfinder_paths/mcp/tools/quotes.py +11 -133
- wayfinder_paths/mcp/tools/wallets.py +4 -7
- wayfinder_paths/mcp/utils.py +0 -22
- wayfinder_paths/policies/lifi.py +5 -2
- wayfinder_paths/policies/moonwell.py +3 -1
- wayfinder_paths/policies/util.py +4 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +2 -4
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +57 -201
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +24 -168
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +24 -63
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -1
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/METADATA +1 -1
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/RECORD +35 -35
- wayfinder_paths/core/types.py +0 -19
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
6
|
-
|
|
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
|
|
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:
|
|
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__)
|
wayfinder_paths/mcp/scripting.py
CHANGED
|
@@ -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
|
|
61
|
-
"Run 'just create-wallets'
|
|
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["
|
|
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 == "
|
|
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 == "
|
|
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="
|
|
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
|
|
261
|
-
all_quotes = quote_data.get("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
|
|
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
|
|
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.
|
|
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
|
-
|
|
209
|
-
if
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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({"
|
|
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
|
-
"
|
|
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=
|
|
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
|
-
"
|
|
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
|
}
|
wayfinder_paths/mcp/utils.py
CHANGED
|
@@ -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
|
|
wayfinder_paths/policies/lifi.py
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
)
|