wayfinder-paths 0.1.23__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 (122) hide show
  1. wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
  2. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
  4. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  5. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  6. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  7. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  8. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  9. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  10. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  11. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  12. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  13. wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  15. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
  16. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
  18. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  19. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
  20. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  21. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  23. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  24. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  27. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  28. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  29. wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
  30. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  31. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
  32. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  33. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  34. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  35. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  36. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  37. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  38. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  39. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  40. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  41. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  42. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  43. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  44. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  45. wayfinder_paths/conftest.py +24 -17
  46. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  47. wayfinder_paths/core/adapters/models.py +17 -7
  48. wayfinder_paths/core/clients/BRAPClient.py +1 -1
  49. wayfinder_paths/core/clients/TokenClient.py +47 -1
  50. wayfinder_paths/core/clients/WayfinderClient.py +1 -2
  51. wayfinder_paths/core/clients/protocols.py +21 -22
  52. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  53. wayfinder_paths/core/config.py +12 -0
  54. wayfinder_paths/core/constants/__init__.py +15 -0
  55. wayfinder_paths/core/constants/base.py +6 -1
  56. wayfinder_paths/core/constants/contracts.py +39 -26
  57. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  58. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  59. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  60. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  61. wayfinder_paths/core/engine/manifest.py +66 -0
  62. wayfinder_paths/core/strategies/Strategy.py +0 -61
  63. wayfinder_paths/core/strategies/__init__.py +10 -1
  64. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  65. wayfinder_paths/core/utils/test_transaction.py +289 -0
  66. wayfinder_paths/core/utils/transaction.py +44 -1
  67. wayfinder_paths/core/utils/web3.py +3 -0
  68. wayfinder_paths/mcp/__init__.py +5 -0
  69. wayfinder_paths/mcp/preview.py +185 -0
  70. wayfinder_paths/mcp/scripting.py +84 -0
  71. wayfinder_paths/mcp/server.py +52 -0
  72. wayfinder_paths/mcp/state/profile_store.py +195 -0
  73. wayfinder_paths/mcp/state/store.py +89 -0
  74. wayfinder_paths/mcp/test_scripting.py +267 -0
  75. wayfinder_paths/mcp/tools/__init__.py +0 -0
  76. wayfinder_paths/mcp/tools/balances.py +290 -0
  77. wayfinder_paths/mcp/tools/discovery.py +158 -0
  78. wayfinder_paths/mcp/tools/execute.py +770 -0
  79. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  80. wayfinder_paths/mcp/tools/quotes.py +288 -0
  81. wayfinder_paths/mcp/tools/run_script.py +286 -0
  82. wayfinder_paths/mcp/tools/strategies.py +188 -0
  83. wayfinder_paths/mcp/tools/tokens.py +46 -0
  84. wayfinder_paths/mcp/tools/wallets.py +354 -0
  85. wayfinder_paths/mcp/utils.py +129 -0
  86. wayfinder_paths/policies/hyperliquid.py +1 -1
  87. wayfinder_paths/policies/lifi.py +18 -0
  88. wayfinder_paths/policies/util.py +8 -2
  89. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
  90. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  91. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  92. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  93. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  94. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  95. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  96. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  97. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  98. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  99. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  100. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  101. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  102. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  103. wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
  104. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  105. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  106. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
  107. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
  108. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
  109. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  110. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
  111. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
  112. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  113. wayfinder_paths/tests/test_test_coverage.py +1 -4
  114. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  115. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  116. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  117. wayfinder_paths/scripts/create_strategy.py +0 -139
  118. wayfinder_paths/scripts/make_wallets.py +0 -142
  119. wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
  120. wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
  121. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  122. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
@@ -0,0 +1,289 @@
1
+ import asyncio
2
+ from unittest.mock import AsyncMock, MagicMock, patch
3
+
4
+ import pytest
5
+ from web3 import AsyncWeb3
6
+
7
+ from wayfinder_paths.core.constants import SUPPORTED_CHAINS
8
+ from wayfinder_paths.core.utils.transaction import (
9
+ PRE_EIP_1559_CHAIN_IDS,
10
+ _get_transaction_from_address,
11
+ gas_limit_transaction,
12
+ gas_price_transaction,
13
+ nonce_transaction,
14
+ )
15
+ from wayfinder_paths.core.utils.web3 import get_transaction_chain_id
16
+
17
+ RANDOM_USER_0 = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
18
+
19
+
20
+ def for_every_chain_id(async_f):
21
+ return asyncio.gather(*[async_f(chain_id) for chain_id in SUPPORTED_CHAINS])
22
+
23
+
24
+ class TestGetChainId:
25
+ def test_valid_chain_id(self):
26
+ transaction = {"chainId": 1}
27
+ result = get_transaction_chain_id(transaction)
28
+ assert result == 1
29
+
30
+ def test_chain_id_as_string(self):
31
+ transaction = {"chainId": "1"}
32
+ result = get_transaction_chain_id(transaction)
33
+ assert result == 1
34
+
35
+ def test_empty_transaction(self):
36
+ transaction = {}
37
+ with pytest.raises(ValueError, match="Transaction does not contain chainId"):
38
+ get_transaction_chain_id(transaction)
39
+
40
+
41
+ class TestGetFromAddress:
42
+ def test_valid_checksum_address(self):
43
+ transaction = {"from": RANDOM_USER_0}
44
+ result = _get_transaction_from_address(transaction)
45
+ assert result == RANDOM_USER_0
46
+ assert AsyncWeb3.is_checksum_address(result)
47
+
48
+ def test_lowercase_address_converted_to_checksum(self):
49
+ lowercase_address = RANDOM_USER_0.lower()
50
+ transaction = {"from": lowercase_address}
51
+ result = _get_transaction_from_address(transaction)
52
+ assert AsyncWeb3.is_checksum_address(result)
53
+ assert result == RANDOM_USER_0
54
+
55
+ def test_empty_transaction(self):
56
+ transaction = {}
57
+ with pytest.raises(
58
+ ValueError, match="Transaction does not contain from address"
59
+ ):
60
+ _get_transaction_from_address(transaction)
61
+
62
+
63
+ @pytest.mark.asyncio
64
+ class TestNonceTransaction:
65
+ @pytest.fixture
66
+ def mock_web3(self):
67
+ web3 = MagicMock()
68
+ web3.eth = MagicMock()
69
+ web3.eth.get_transaction_count = AsyncMock()
70
+ return web3
71
+
72
+ async def test_noncing_on_mainnet(self):
73
+ transaction = {
74
+ "from": RANDOM_USER_0,
75
+ "chainId": 1,
76
+ }
77
+ result = await nonce_transaction(transaction)
78
+ assert result["nonce"] >= 0
79
+
80
+ async def test_noncing_on_all_chains(self):
81
+ async def test_noncing(chain_id):
82
+ transaction = {
83
+ "from": RANDOM_USER_0,
84
+ "chainId": chain_id,
85
+ }
86
+ result = await nonce_transaction(transaction)
87
+ assert "nonce" in result
88
+ assert result["nonce"] >= 0
89
+
90
+ await for_every_chain_id(test_noncing)
91
+
92
+ @patch("wayfinder_paths.core.utils.transaction.web3s_from_chain_id")
93
+ async def test_multiple_web3s_returns_max_nonce(self, mock_web3s_context):
94
+ mock_web3_1 = MagicMock()
95
+ mock_web3_1.eth = MagicMock()
96
+ mock_web3_1.eth.get_transaction_count = AsyncMock(return_value=5)
97
+ mock_web3_1.provider.disconnect = AsyncMock()
98
+
99
+ mock_web3_2 = MagicMock()
100
+ mock_web3_2.eth = MagicMock()
101
+ mock_web3_2.eth.get_transaction_count = AsyncMock(return_value=8)
102
+ mock_web3_2.provider.disconnect = AsyncMock()
103
+
104
+ mock_web3_3 = MagicMock()
105
+ mock_web3_3.eth = MagicMock()
106
+ mock_web3_3.eth.get_transaction_count = AsyncMock(return_value=6)
107
+ mock_web3_3.provider.disconnect = AsyncMock()
108
+
109
+ mock_web3s_context.return_value.__aenter__.return_value = [
110
+ mock_web3_1,
111
+ mock_web3_2,
112
+ mock_web3_3,
113
+ ]
114
+
115
+ transaction = {
116
+ "from": RANDOM_USER_0,
117
+ "chainId": 1,
118
+ }
119
+
120
+ result = await nonce_transaction(transaction)
121
+
122
+ assert result["nonce"] == 8
123
+ mock_web3_1.eth.get_transaction_count.assert_called_once()
124
+ mock_web3_2.eth.get_transaction_count.assert_called_once()
125
+ mock_web3_3.eth.get_transaction_count.assert_called_once()
126
+
127
+ @patch("wayfinder_paths.core.utils.transaction.web3s_from_chain_id")
128
+ async def test_preserves_all_existing_fields(self, mock_web3s_context, mock_web3):
129
+ mock_web3.eth.get_transaction_count.return_value = 5
130
+ mock_web3.provider.disconnect = AsyncMock()
131
+ mock_web3s_context.return_value.__aenter__.return_value = [mock_web3]
132
+
133
+ transaction = {
134
+ "from": RANDOM_USER_0,
135
+ "chainId": 1,
136
+ "to": RANDOM_USER_0,
137
+ "value": 100,
138
+ "gas": 21000,
139
+ "gasPrice": 1000000000,
140
+ "data": "0xabcd",
141
+ }
142
+
143
+ result = await nonce_transaction(transaction)
144
+
145
+ assert result["nonce"] == 5
146
+ assert result["from"] == transaction["from"]
147
+ assert result["chainId"] == transaction["chainId"]
148
+ assert result["to"] == transaction["to"]
149
+ assert result["value"] == transaction["value"]
150
+ assert result["gas"] == transaction["gas"]
151
+ assert result["gasPrice"] == transaction["gasPrice"]
152
+ assert result["data"] == transaction["data"]
153
+
154
+
155
+ @pytest.mark.asyncio
156
+ class TestGasPriceTransaction:
157
+ async def test_pricing_on_all_chains(self):
158
+ async def test_pricing(chain_id):
159
+ transaction = {
160
+ "chainId": chain_id,
161
+ }
162
+ result = await gas_price_transaction(transaction)
163
+ if chain_id in PRE_EIP_1559_CHAIN_IDS:
164
+ assert "maxFeePerGas" not in result
165
+ assert "maxPriorityFeePerGas" not in result
166
+ assert result["gasPrice"] > 0
167
+ elif chain_id == 999:
168
+ assert "gasPrice" not in result
169
+ assert result["maxFeePerGas"] > 0
170
+ assert result["maxPriorityFeePerGas"] == 0
171
+ else:
172
+ assert "gasPrice" not in result
173
+ assert result["maxFeePerGas"] > 0
174
+ assert result["maxPriorityFeePerGas"] > 0
175
+
176
+ await for_every_chain_id(test_pricing)
177
+
178
+ @patch("wayfinder_paths.core.utils.transaction.web3s_from_chain_id")
179
+ async def test_eip1559_with_custom_multiplier_and_max_aggregation(
180
+ self, mock_web3s_context
181
+ ):
182
+ # Mock multiple web3 instances with different base fees and priority fees
183
+ mock_block_1 = MagicMock()
184
+ mock_block_1.baseFeePerGas = 30_000_000_000
185
+ mock_fee_history_1 = MagicMock()
186
+ mock_fee_history_1.reward = [[2_000_000_000] for _ in range(10)]
187
+ mock_web3_1 = MagicMock()
188
+ mock_web3_1.eth = MagicMock()
189
+ mock_web3_1.eth.get_block = AsyncMock(return_value=mock_block_1)
190
+ mock_web3_1.eth.fee_history = AsyncMock(return_value=mock_fee_history_1)
191
+ mock_web3_1.provider.disconnect = AsyncMock()
192
+
193
+ mock_block_2 = MagicMock()
194
+ mock_block_2.baseFeePerGas = 35_000_000_000
195
+ mock_fee_history_2 = MagicMock()
196
+ mock_fee_history_2.reward = [[3_000_000_000] for _ in range(10)]
197
+ mock_web3_2 = MagicMock()
198
+ mock_web3_2.eth = MagicMock()
199
+ mock_web3_2.eth.get_block = AsyncMock(return_value=mock_block_2)
200
+ mock_web3_2.eth.fee_history = AsyncMock(return_value=mock_fee_history_2)
201
+ mock_web3_2.provider.disconnect = AsyncMock()
202
+
203
+ mock_block_3 = MagicMock()
204
+ mock_block_3.baseFeePerGas = 32_000_000_000
205
+ mock_fee_history_3 = MagicMock()
206
+ mock_fee_history_3.reward = [[2_500_000_000] for _ in range(10)]
207
+ mock_web3_3 = MagicMock()
208
+ mock_web3_3.eth = MagicMock()
209
+ mock_web3_3.eth.get_block = AsyncMock(return_value=mock_block_3)
210
+ mock_web3_3.eth.fee_history = AsyncMock(return_value=mock_fee_history_3)
211
+ mock_web3_3.provider.disconnect = AsyncMock()
212
+
213
+ mock_web3s_context.return_value.__aenter__.return_value = [
214
+ mock_web3_1,
215
+ mock_web3_2,
216
+ mock_web3_3,
217
+ ]
218
+
219
+ transaction = {"chainId": 1}
220
+ custom_priority_multiplier = 2.0
221
+
222
+ result = await gas_price_transaction(
223
+ transaction, priority_fee_multiplier=custom_priority_multiplier
224
+ )
225
+
226
+ # Should use max base fee (35 gwei) and max priority fee (3 gwei)
227
+ expected_max_priority_fee = int(3_000_000_000 * custom_priority_multiplier)
228
+ expected_max_fee = int(
229
+ 35_000_000_000 * 2 + 3_000_000_000 * custom_priority_multiplier
230
+ )
231
+
232
+ assert result["maxPriorityFeePerGas"] == expected_max_priority_fee
233
+ assert result["maxFeePerGas"] == expected_max_fee
234
+ assert "gasPrice" not in result
235
+
236
+ @patch("wayfinder_paths.core.utils.transaction.web3s_from_chain_id")
237
+ async def test_non_eip1559_with_custom_multiplier_and_max_aggregation(
238
+ self, mock_web3s_context
239
+ ):
240
+ # Mock multiple web3 instances with different gas prices
241
+ # gas_price is an awaitable property, so we need to make it a coroutine
242
+ mock_web3_1 = MagicMock()
243
+ mock_web3_1.eth = MagicMock()
244
+ mock_web3_1.eth.gas_price = AsyncMock(return_value=5_000_000_000)()
245
+ mock_web3_1.provider.disconnect = AsyncMock()
246
+
247
+ mock_web3_2 = MagicMock()
248
+ mock_web3_2.eth = MagicMock()
249
+ mock_web3_2.eth.gas_price = AsyncMock(return_value=8_000_000_000)()
250
+ mock_web3_2.provider.disconnect = AsyncMock()
251
+
252
+ mock_web3_3 = MagicMock()
253
+ mock_web3_3.eth = MagicMock()
254
+ mock_web3_3.eth.gas_price = AsyncMock(return_value=6_000_000_000)()
255
+ mock_web3_3.provider.disconnect = AsyncMock()
256
+
257
+ mock_web3s_context.return_value.__aenter__.return_value = [
258
+ mock_web3_1,
259
+ mock_web3_2,
260
+ mock_web3_3,
261
+ ]
262
+
263
+ transaction = {"chainId": 56}
264
+ custom_gas_multiplier = 2.5
265
+
266
+ result = await gas_price_transaction(
267
+ transaction, gas_price_multiplier=custom_gas_multiplier
268
+ )
269
+
270
+ # Should use max gas price (8 gwei) * multiplier
271
+ expected_gas_price = int(8_000_000_000 * custom_gas_multiplier)
272
+
273
+ assert result["gasPrice"] == expected_gas_price
274
+ assert "maxFeePerGas" not in result
275
+ assert "maxPriorityFeePerGas" not in result
276
+
277
+
278
+ @pytest.mark.asyncio
279
+ class TestGasLimitTransaction:
280
+ async def test_gas_limit_on_all_chains(self):
281
+ async def test_gas_limit(chain_id):
282
+ transaction = {
283
+ "chainId": chain_id,
284
+ }
285
+ result = await gas_limit_transaction(transaction)
286
+ assert "gas" in result
287
+ assert result["gas"] > 0
288
+
289
+ await for_every_chain_id(test_gas_limit)
@@ -1,6 +1,8 @@
1
1
  import asyncio
2
2
  from collections.abc import Callable
3
+ from typing import Any
3
4
 
5
+ from eth_account import Account
4
6
  from loguru import logger
5
7
  from web3 import AsyncWeb3
6
8
 
@@ -180,7 +182,7 @@ async def send_transaction(
180
182
  if sign_callback is None:
181
183
  raise ValueError("sign_callback must be provided to send transaction")
182
184
 
183
- logger.info(f"Broadcasting transaction {transaction.get('to', 'unknown')[:10]}...")
185
+ logger.info(f"Broadcasting transaction {transaction}...")
184
186
  chain_id = get_transaction_chain_id(transaction)
185
187
  transaction = await gas_limit_transaction(transaction)
186
188
  transaction = await nonce_transaction(transaction)
@@ -193,4 +195,45 @@ async def send_transaction(
193
195
  return txn_hash
194
196
 
195
197
 
198
+ async def sign_and_send_transaction(
199
+ transaction: dict, private_key: str, wait_for_receipt: bool = True
200
+ ) -> str:
201
+ account = Account.from_key(private_key)
202
+
203
+ async def sign_callback(tx: dict) -> bytes:
204
+ signed = account.sign_transaction(tx)
205
+ return signed.raw_transaction
206
+
207
+ return await send_transaction(transaction, sign_callback, wait_for_receipt)
208
+
209
+
210
+ async def encode_call(
211
+ *,
212
+ target: str,
213
+ abi: list[dict[str, Any]],
214
+ fn_name: str,
215
+ args: list[Any],
216
+ from_address: str,
217
+ chain_id: int,
218
+ value: int = 0,
219
+ ) -> dict[str, Any]:
220
+ async with web3_from_chain_id(chain_id) as web3:
221
+ contract = web3.eth.contract(address=target, abi=abi)
222
+ try:
223
+ tx_data = await getattr(contract.functions, fn_name)(
224
+ *args
225
+ ).build_transaction({"from": from_address})
226
+ data = tx_data["data"]
227
+ except ValueError as exc:
228
+ raise ValueError(f"Failed to encode {fn_name}: {exc}") from exc
229
+
230
+ return {
231
+ "chainId": int(chain_id),
232
+ "from": AsyncWeb3.to_checksum_address(from_address),
233
+ "to": AsyncWeb3.to_checksum_address(target),
234
+ "data": data,
235
+ "value": int(value),
236
+ }
237
+
238
+
196
239
  # TODO: HypeEVM Big Blocks: Setting and detecting
@@ -26,6 +26,9 @@ def _get_rpcs_for_chain_id(chain_id: int) -> list:
26
26
  rpcs = get_rpc_urls().get(str(chain_id))
27
27
  if rpcs is None:
28
28
  raise ValueError(f"No RPCs configured for chain ID {chain_id}")
29
+ # Handle both string (single URL) and list (multiple URLs) formats
30
+ if isinstance(rpcs, str):
31
+ return [rpcs]
29
32
  return rpcs
30
33
 
31
34
 
@@ -0,0 +1,5 @@
1
+ """Wayfinder Paths MCP server package.
2
+
3
+ This package is intentionally isolated from the core SDK so that importing
4
+ `wayfinder_paths` does not require MCP dependencies.
5
+ """
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from wayfinder_paths.core.constants.hyperliquid import (
8
+ ARBITRUM_USDC_TOKEN_ID,
9
+ HYPE_FEE_WALLET,
10
+ HYPERLIQUID_BRIDGE_ADDRESS,
11
+ )
12
+ from wayfinder_paths.mcp.utils import (
13
+ find_wallet_by_label,
14
+ normalize_address,
15
+ read_text_excerpt,
16
+ repo_root,
17
+ )
18
+
19
+
20
+ def build_execution_preview(tool_input: dict[str, Any]) -> dict[str, Any]:
21
+ req = tool_input.get("request") if isinstance(tool_input, dict) else None
22
+ if not isinstance(req, dict):
23
+ return {
24
+ "summary": "Execute request missing 'request' object.",
25
+ "recipient_mismatch": False,
26
+ }
27
+
28
+ kind = str(req.get("kind") or "").strip()
29
+ wallet_label = str(req.get("wallet_label") or "").strip()
30
+ w = find_wallet_by_label(wallet_label) if wallet_label else None
31
+ sender = normalize_address((w or {}).get("address")) if w else None
32
+
33
+ recipient = normalize_address(req.get("recipient"))
34
+ if kind == "swap":
35
+ recipient = recipient or sender
36
+ summary = (
37
+ "EXECUTE swap\n"
38
+ f"wallet_label: {wallet_label}\n"
39
+ f"from_token: {req.get('from_token')}\n"
40
+ f"to_token: {req.get('to_token')}\n"
41
+ f"amount: {req.get('amount')}\n"
42
+ f"slippage_bps: {req.get('slippage_bps')}\n"
43
+ f"sender: {sender or '(unknown)'}\n"
44
+ f"recipient: {recipient or '(unknown)'}"
45
+ )
46
+ elif kind == "hyperliquid_deposit":
47
+ recipient = normalize_address(HYPERLIQUID_BRIDGE_ADDRESS)
48
+ summary = (
49
+ "EXECUTE hyperliquid_deposit (Bridge2)\n"
50
+ f"wallet_label: {wallet_label}\n"
51
+ f"token: {ARBITRUM_USDC_TOKEN_ID}\n"
52
+ f"amount: {req.get('amount')}\n"
53
+ "chain_id: 42161\n"
54
+ f"sender: {sender or '(unknown)'}\n"
55
+ f"recipient: {recipient or '(missing)'}"
56
+ )
57
+ elif kind == "send":
58
+ summary = (
59
+ "EXECUTE send\n"
60
+ f"wallet_label: {wallet_label}\n"
61
+ f"token: {req.get('token')}\n"
62
+ f"amount: {req.get('amount')}\n"
63
+ f"chain_id: {req.get('chain_id')}\n"
64
+ f"sender: {sender or '(unknown)'}\n"
65
+ f"recipient: {recipient or '(missing)'}"
66
+ )
67
+ else:
68
+ summary = f"EXECUTE {kind or '(unknown kind)'}\nwallet_label: {wallet_label}"
69
+
70
+ mismatch = bool(sender and recipient and sender.lower() != recipient.lower())
71
+ if kind == "hyperliquid_deposit":
72
+ mismatch = False # deposit recipient is fixed; mismatch is expected
73
+ return {"summary": summary, "recipient_mismatch": mismatch}
74
+
75
+
76
+ def build_run_script_preview(tool_input: dict[str, Any]) -> dict[str, Any]:
77
+ ti = tool_input if isinstance(tool_input, dict) else {}
78
+ path_raw = ti.get("script_path") or ti.get("path")
79
+ args = ti.get("args") if isinstance(ti.get("args"), list) else []
80
+ dry_run = bool(ti.get("dry_run", True))
81
+
82
+ if not isinstance(path_raw, str) or not path_raw.strip():
83
+ return {"summary": "RUN_SCRIPT missing script_path."}
84
+
85
+ root = repo_root()
86
+ p = Path(path_raw)
87
+ if not p.is_absolute():
88
+ p = root / p
89
+ resolved = p.resolve(strict=False)
90
+
91
+ rel = str(resolved)
92
+ try:
93
+ rel = str(resolved.relative_to(root))
94
+ except Exception:
95
+ pass
96
+
97
+ sha = None
98
+ try:
99
+ if resolved.exists():
100
+ sha = hashlib.sha256(resolved.read_bytes()).hexdigest()
101
+ except Exception:
102
+ sha = None
103
+
104
+ excerpt = read_text_excerpt(resolved, max_chars=1200) if resolved.exists() else None
105
+
106
+ summary = (
107
+ "RUN_SCRIPT (executes local python)\n"
108
+ f"script_path: {rel}\n"
109
+ f"dry_run: {dry_run}\n"
110
+ f"args: {args or []}\n"
111
+ f"script_sha256: {(sha[:12] + '…') if sha else '(unavailable)'}"
112
+ )
113
+ if excerpt:
114
+ summary += "\n\n" + excerpt
115
+ else:
116
+ summary += "\n\n(no script contents available)"
117
+
118
+ return {"summary": summary}
119
+
120
+
121
+ def build_hyperliquid_execute_preview(tool_input: dict[str, Any]) -> dict[str, Any]:
122
+ # hyperliquid_execute uses direct parameters, not a 'request' wrapper
123
+ req = tool_input if isinstance(tool_input, dict) else {}
124
+ if not req:
125
+ return {"summary": "HYPERLIQUID_EXECUTE missing parameters."}
126
+
127
+ action = str(req.get("action") or "").strip()
128
+ wallet_label = str(req.get("wallet_label") or "").strip()
129
+ w = find_wallet_by_label(wallet_label) if wallet_label else None
130
+ sender = normalize_address((w or {}).get("address")) if w else None
131
+
132
+ dry_run = req.get("dry_run")
133
+ coin = req.get("coin")
134
+ asset_id = req.get("asset_id")
135
+
136
+ header = "HYPERLIQUID_EXECUTE\n"
137
+ base = (
138
+ f"action: {action or '(missing)'}\n"
139
+ f"wallet_label: {wallet_label}\n"
140
+ f"address: {sender or '(unknown)'}\n"
141
+ f"dry_run: {dry_run}\n"
142
+ f"coin: {coin}\n"
143
+ f"asset_id: {asset_id}"
144
+ )
145
+
146
+ if action == "place_order":
147
+ details = (
148
+ "\n\nORDER\n"
149
+ f"order_type: {req.get('order_type')}\n"
150
+ f"is_buy: {req.get('is_buy')}\n"
151
+ f"size: {req.get('size')}\n"
152
+ f"usd_amount: {req.get('usd_amount')}\n"
153
+ f"usd_amount_kind: {req.get('usd_amount_kind')}\n"
154
+ f"price: {req.get('price')}\n"
155
+ f"slippage: {req.get('slippage')}\n"
156
+ f"reduce_only: {req.get('reduce_only')}\n"
157
+ f"cloid: {req.get('cloid')}\n"
158
+ f"leverage: {req.get('leverage')}\n"
159
+ f"is_cross: {req.get('is_cross')}\n"
160
+ f"builder_wallet: {HYPE_FEE_WALLET}\n"
161
+ f"builder_fee_tenths_bp: {req.get('builder_fee_tenths_bp') or '(from config/default)'}"
162
+ )
163
+ return {"summary": header + base + details}
164
+
165
+ if action == "cancel_order":
166
+ details = (
167
+ "\n\nCANCEL\n"
168
+ f"order_id: {req.get('order_id')}\n"
169
+ f"cancel_cloid: {req.get('cancel_cloid')}"
170
+ )
171
+ return {"summary": header + base + details}
172
+
173
+ if action == "update_leverage":
174
+ details = (
175
+ "\n\nLEVERAGE\n"
176
+ f"leverage: {req.get('leverage')}\n"
177
+ f"is_cross: {req.get('is_cross')}"
178
+ )
179
+ return {"summary": header + base + details}
180
+
181
+ if action == "withdraw":
182
+ details = f"\n\nWITHDRAW\namount_usdc: {req.get('amount_usdc')}"
183
+ return {"summary": header + base + details}
184
+
185
+ return {"summary": header + base}
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Any
5
+
6
+ from eth_account import Account
7
+
8
+ from wayfinder_paths.mcp.utils import find_wallet_by_label, load_config_json
9
+
10
+ # Known signing callback parameter names used by adapters
11
+ _SIGNING_CALLBACK_PARAMS = frozenset(
12
+ {
13
+ "strategy_wallet_signing_callback",
14
+ "sign_callback",
15
+ "signing_callback",
16
+ }
17
+ )
18
+
19
+
20
+ def _make_sign_callback(private_key: str):
21
+ account = Account.from_key(private_key)
22
+
23
+ async def sign_callback(transaction: dict) -> bytes:
24
+ signed = account.sign_transaction(transaction)
25
+ return signed.raw_transaction
26
+
27
+ return sign_callback
28
+
29
+
30
+ def _detect_callback_params(adapter_class: type) -> set[str]:
31
+ try:
32
+ sig = inspect.signature(adapter_class.__init__)
33
+ except (ValueError, TypeError):
34
+ return set()
35
+
36
+ return {
37
+ name
38
+ for name in sig.parameters
39
+ if name in _SIGNING_CALLBACK_PARAMS or name.endswith("_signing_callback")
40
+ }
41
+
42
+
43
+ def get_adapter[T](
44
+ adapter_class: type[T],
45
+ wallet_label: str | None = None,
46
+ *,
47
+ config_overrides: dict[str, Any] | None = None,
48
+ **kwargs: Any,
49
+ ) -> T:
50
+ config = load_config_json()
51
+
52
+ if config_overrides:
53
+ config = {**config, **config_overrides}
54
+
55
+ sign_callback = None
56
+ if wallet_label:
57
+ wallet = find_wallet_by_label(wallet_label)
58
+ if not wallet:
59
+ raise ValueError(
60
+ f"Wallet '{wallet_label}' not found in wallets.json. "
61
+ "Run 'just create-wallets' or check WALLETS_PATH."
62
+ )
63
+
64
+ private_key = wallet.get("private_key") or wallet.get("private_key_hex")
65
+ if not private_key:
66
+ raise ValueError(
67
+ f"Wallet '{wallet_label}' is missing private_key_hex. "
68
+ "Local signing requires a private key."
69
+ )
70
+
71
+ config["strategy_wallet"] = wallet
72
+ sign_callback = _make_sign_callback(private_key)
73
+
74
+ callback_params = _detect_callback_params(adapter_class)
75
+ adapter_kwargs: dict[str, Any] = {"config": config}
76
+
77
+ if sign_callback and callback_params:
78
+ for param_name in callback_params:
79
+ if param_name not in kwargs:
80
+ adapter_kwargs[param_name] = sign_callback
81
+
82
+ adapter_kwargs.update(kwargs)
83
+
84
+ return adapter_class(**adapter_kwargs)
@@ -0,0 +1,52 @@
1
+ """Wayfinder Paths MCP server (FastMCP).
2
+
3
+ Run locally (via Claude Code .mcp.json):
4
+ poetry run python -m wayfinder_paths.mcp.server
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+
11
+ from mcp.server.fastmcp import FastMCP
12
+
13
+ from wayfinder_paths.mcp.tools.balances import balances
14
+ from wayfinder_paths.mcp.tools.discovery import describe, discover
15
+ from wayfinder_paths.mcp.tools.execute import execute
16
+ from wayfinder_paths.mcp.tools.hyperliquid import hyperliquid, hyperliquid_execute
17
+ from wayfinder_paths.mcp.tools.quotes import quote_swap
18
+ from wayfinder_paths.mcp.tools.run_script import run_script
19
+ from wayfinder_paths.mcp.tools.strategies import run_strategy
20
+ from wayfinder_paths.mcp.tools.tokens import tokens
21
+ from wayfinder_paths.mcp.tools.wallets import wallets
22
+
23
+ mcp = FastMCP("wayfinder")
24
+
25
+ mcp.tool()(discover)
26
+ mcp.tool()(describe)
27
+ mcp.tool()(wallets)
28
+ mcp.tool()(tokens)
29
+ mcp.tool()(balances)
30
+ mcp.tool()(quote_swap)
31
+ mcp.tool()(hyperliquid)
32
+ mcp.tool()(hyperliquid_execute)
33
+ mcp.tool()(run_strategy)
34
+ mcp.tool()(run_script)
35
+ mcp.tool()(execute)
36
+
37
+
38
+ def main() -> None:
39
+ # FastMCP is sync, but our tools are async; the library handles that.
40
+ mcp.run()
41
+
42
+
43
+ if __name__ == "__main__":
44
+ # Some environments complain if an event loop is already running.
45
+ # FastMCP handles stdio and tool execution; we just start it.
46
+ try:
47
+ main()
48
+ except RuntimeError as exc:
49
+ if "asyncio.run()" in str(exc) and asyncio.get_event_loop().is_running():
50
+ main()
51
+ else:
52
+ raise