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.
- wayfinder_paths/__init__.py +0 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/__init__.py +0 -3
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +4 -1
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -3
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +21 -35
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +10 -162
- wayfinder_paths/core/constants/__init__.py +73 -2
- wayfinder_paths/core/constants/base.py +8 -17
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +52 -0
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/constants/tokens.py +9 -0
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -71
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +57 -8
- wayfinder_paths/core/utils/web3.py +8 -3
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
- wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Tests for wayfinder_paths.mcp.scripting module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from wayfinder_paths.adapters.boros_adapter import BorosAdapter
|
|
10
|
+
from wayfinder_paths.adapters.hyperlend_adapter import HyperlendAdapter
|
|
11
|
+
from wayfinder_paths.adapters.moonwell_adapter import MoonwellAdapter
|
|
12
|
+
from wayfinder_paths.adapters.pendle_adapter import PendleAdapter
|
|
13
|
+
from wayfinder_paths.mcp.scripting import (
|
|
14
|
+
_detect_callback_params,
|
|
15
|
+
_make_sign_callback,
|
|
16
|
+
get_adapter,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestDetectCallbackParams:
|
|
21
|
+
"""Tests for _detect_callback_params function."""
|
|
22
|
+
|
|
23
|
+
def test_detects_strategy_wallet_signing_callback(self):
|
|
24
|
+
"""Should detect strategy_wallet_signing_callback parameter."""
|
|
25
|
+
|
|
26
|
+
class MockAdapter:
|
|
27
|
+
def __init__(self, config=None, strategy_wallet_signing_callback=None):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
result = _detect_callback_params(MockAdapter)
|
|
31
|
+
assert "strategy_wallet_signing_callback" in result
|
|
32
|
+
|
|
33
|
+
def test_detects_sign_callback(self):
|
|
34
|
+
"""Should detect sign_callback parameter."""
|
|
35
|
+
|
|
36
|
+
class MockAdapter:
|
|
37
|
+
def __init__(self, config=None, *, sign_callback=None):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
result = _detect_callback_params(MockAdapter)
|
|
41
|
+
assert "sign_callback" in result
|
|
42
|
+
|
|
43
|
+
def test_detects_custom_signing_callback_suffix(self):
|
|
44
|
+
"""Should detect params ending with _signing_callback."""
|
|
45
|
+
|
|
46
|
+
class MockAdapter:
|
|
47
|
+
def __init__(self, config=None, custom_signing_callback=None):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
result = _detect_callback_params(MockAdapter)
|
|
51
|
+
assert "custom_signing_callback" in result
|
|
52
|
+
|
|
53
|
+
def test_returns_empty_for_no_callback_params(self):
|
|
54
|
+
"""Should return empty set when no callback params found."""
|
|
55
|
+
|
|
56
|
+
class MockAdapter:
|
|
57
|
+
def __init__(self, config=None, timeout=30):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
result = _detect_callback_params(MockAdapter)
|
|
61
|
+
assert result == set()
|
|
62
|
+
|
|
63
|
+
def test_detects_multiple_callback_params(self):
|
|
64
|
+
"""Should detect all matching callback params."""
|
|
65
|
+
|
|
66
|
+
class MockAdapter:
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
config=None,
|
|
70
|
+
strategy_wallet_signing_callback=None,
|
|
71
|
+
sign_callback=None,
|
|
72
|
+
):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
result = _detect_callback_params(MockAdapter)
|
|
76
|
+
assert "strategy_wallet_signing_callback" in result
|
|
77
|
+
assert "sign_callback" in result
|
|
78
|
+
|
|
79
|
+
def test_real_moonwell_adapter(self):
|
|
80
|
+
"""Should detect callback param from MoonwellAdapter."""
|
|
81
|
+
result = _detect_callback_params(MoonwellAdapter)
|
|
82
|
+
assert "strategy_wallet_signing_callback" in result
|
|
83
|
+
|
|
84
|
+
def test_real_boros_adapter(self):
|
|
85
|
+
"""Should detect callback param from BorosAdapter."""
|
|
86
|
+
result = _detect_callback_params(BorosAdapter)
|
|
87
|
+
assert "sign_callback" in result
|
|
88
|
+
|
|
89
|
+
def test_real_hyperlend_adapter(self):
|
|
90
|
+
"""Should detect callback param from HyperlendAdapter."""
|
|
91
|
+
result = _detect_callback_params(HyperlendAdapter)
|
|
92
|
+
assert "strategy_wallet_signing_callback" in result
|
|
93
|
+
|
|
94
|
+
def test_real_pendle_adapter(self):
|
|
95
|
+
"""Should detect callback param from PendleAdapter."""
|
|
96
|
+
result = _detect_callback_params(PendleAdapter)
|
|
97
|
+
assert "strategy_wallet_signing_callback" in result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestMakeSignCallback:
|
|
101
|
+
"""Tests for _make_sign_callback function."""
|
|
102
|
+
|
|
103
|
+
@pytest.mark.asyncio
|
|
104
|
+
async def test_creates_working_callback(self):
|
|
105
|
+
"""Should create a callback that signs transactions."""
|
|
106
|
+
# Test private key (DO NOT USE IN PRODUCTION)
|
|
107
|
+
test_pk = "0x" + "ab" * 32
|
|
108
|
+
|
|
109
|
+
callback = _make_sign_callback(test_pk)
|
|
110
|
+
|
|
111
|
+
# Create a minimal transaction
|
|
112
|
+
tx = {
|
|
113
|
+
"to": "0x" + "00" * 20,
|
|
114
|
+
"value": 0,
|
|
115
|
+
"gas": 21000,
|
|
116
|
+
"gasPrice": 1000000000,
|
|
117
|
+
"nonce": 0,
|
|
118
|
+
"chainId": 1,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
result = await callback(tx)
|
|
122
|
+
assert isinstance(result, bytes)
|
|
123
|
+
assert len(result) > 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestGetAdapter:
|
|
127
|
+
"""Tests for get_adapter function."""
|
|
128
|
+
|
|
129
|
+
def test_raises_when_wallet_not_found(self):
|
|
130
|
+
"""Should raise ValueError when wallet label not found."""
|
|
131
|
+
|
|
132
|
+
class MockAdapter:
|
|
133
|
+
def __init__(self, config=None):
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
with patch(
|
|
137
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=None
|
|
138
|
+
):
|
|
139
|
+
with pytest.raises(ValueError, match="not found"):
|
|
140
|
+
get_adapter(MockAdapter, "nonexistent")
|
|
141
|
+
|
|
142
|
+
def test_raises_when_wallet_missing_private_key(self):
|
|
143
|
+
"""Should raise ValueError when wallet has no private key."""
|
|
144
|
+
|
|
145
|
+
class MockAdapter:
|
|
146
|
+
def __init__(self, config=None):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
wallet = {"label": "test", "address": "0x" + "11" * 20}
|
|
150
|
+
|
|
151
|
+
with patch(
|
|
152
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=wallet
|
|
153
|
+
):
|
|
154
|
+
with pytest.raises(ValueError, match="missing private_key"):
|
|
155
|
+
get_adapter(MockAdapter, "test")
|
|
156
|
+
|
|
157
|
+
def test_works_without_wallet_for_readonly(self):
|
|
158
|
+
"""Should work without wallet for read-only adapters."""
|
|
159
|
+
|
|
160
|
+
class MockAdapter:
|
|
161
|
+
def __init__(self, config=None):
|
|
162
|
+
self.config = config
|
|
163
|
+
|
|
164
|
+
with patch(
|
|
165
|
+
"wayfinder_paths.mcp.scripting.load_config_json",
|
|
166
|
+
return_value={"foo": "bar"},
|
|
167
|
+
):
|
|
168
|
+
adapter = get_adapter(MockAdapter)
|
|
169
|
+
assert adapter.config == {"foo": "bar"}
|
|
170
|
+
|
|
171
|
+
def test_wires_strategy_wallet_into_config(self):
|
|
172
|
+
"""Should wire wallet into config['strategy_wallet']."""
|
|
173
|
+
|
|
174
|
+
class MockAdapter:
|
|
175
|
+
def __init__(self, config=None, strategy_wallet_signing_callback=None):
|
|
176
|
+
self.config = config
|
|
177
|
+
self.callback = strategy_wallet_signing_callback
|
|
178
|
+
|
|
179
|
+
wallet = {
|
|
180
|
+
"label": "main",
|
|
181
|
+
"address": "0x" + "11" * 20,
|
|
182
|
+
"private_key_hex": "0x" + "ab" * 32,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
with patch(
|
|
186
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=wallet
|
|
187
|
+
):
|
|
188
|
+
with patch(
|
|
189
|
+
"wayfinder_paths.mcp.scripting.load_config_json", return_value={}
|
|
190
|
+
):
|
|
191
|
+
adapter = get_adapter(MockAdapter, "main")
|
|
192
|
+
assert adapter.config["strategy_wallet"] == wallet
|
|
193
|
+
assert adapter.callback is not None
|
|
194
|
+
|
|
195
|
+
def test_applies_config_overrides(self):
|
|
196
|
+
"""Should merge config_overrides into loaded config."""
|
|
197
|
+
|
|
198
|
+
class MockAdapter:
|
|
199
|
+
def __init__(self, config=None):
|
|
200
|
+
self.config = config
|
|
201
|
+
|
|
202
|
+
with patch(
|
|
203
|
+
"wayfinder_paths.mcp.scripting.load_config_json",
|
|
204
|
+
return_value={"base": "value"},
|
|
205
|
+
):
|
|
206
|
+
adapter = get_adapter(MockAdapter, config_overrides={"override": "yes"})
|
|
207
|
+
assert adapter.config["base"] == "value"
|
|
208
|
+
assert adapter.config["override"] == "yes"
|
|
209
|
+
|
|
210
|
+
def test_passes_kwargs_to_adapter(self):
|
|
211
|
+
"""Should pass additional kwargs to adapter constructor."""
|
|
212
|
+
|
|
213
|
+
class MockAdapter:
|
|
214
|
+
def __init__(self, config=None, custom_arg=None):
|
|
215
|
+
self.config = config
|
|
216
|
+
self.custom_arg = custom_arg
|
|
217
|
+
|
|
218
|
+
with patch("wayfinder_paths.mcp.scripting.load_config_json", return_value={}):
|
|
219
|
+
adapter = get_adapter(MockAdapter, custom_arg="my_value")
|
|
220
|
+
assert adapter.custom_arg == "my_value"
|
|
221
|
+
|
|
222
|
+
def test_caller_kwargs_override_auto_wired_callback(self):
|
|
223
|
+
"""Should allow caller to override auto-wired signing callback."""
|
|
224
|
+
|
|
225
|
+
class MockAdapter:
|
|
226
|
+
def __init__(self, config=None, strategy_wallet_signing_callback=None):
|
|
227
|
+
self.callback = strategy_wallet_signing_callback
|
|
228
|
+
|
|
229
|
+
wallet = {
|
|
230
|
+
"label": "main",
|
|
231
|
+
"address": "0x" + "11" * 20,
|
|
232
|
+
"private_key_hex": "0x" + "ab" * 32,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
custom_callback = MagicMock()
|
|
236
|
+
|
|
237
|
+
with patch(
|
|
238
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=wallet
|
|
239
|
+
):
|
|
240
|
+
with patch(
|
|
241
|
+
"wayfinder_paths.mcp.scripting.load_config_json", return_value={}
|
|
242
|
+
):
|
|
243
|
+
adapter = get_adapter(
|
|
244
|
+
MockAdapter,
|
|
245
|
+
"main",
|
|
246
|
+
strategy_wallet_signing_callback=custom_callback,
|
|
247
|
+
)
|
|
248
|
+
assert adapter.callback is custom_callback
|
|
249
|
+
|
|
250
|
+
def test_integration_with_real_adapter_mocked_wallet(self):
|
|
251
|
+
"""Integration test with real adapter class and mocked wallet."""
|
|
252
|
+
wallet = {
|
|
253
|
+
"label": "main",
|
|
254
|
+
"address": "0x" + "11" * 20,
|
|
255
|
+
"private_key_hex": "0x" + "ab" * 32,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
with patch(
|
|
259
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=wallet
|
|
260
|
+
):
|
|
261
|
+
with patch(
|
|
262
|
+
"wayfinder_paths.mcp.scripting.load_config_json", return_value={}
|
|
263
|
+
):
|
|
264
|
+
adapter = get_adapter(MoonwellAdapter, "main")
|
|
265
|
+
assert isinstance(adapter, MoonwellAdapter)
|
|
266
|
+
assert adapter.strategy_wallet_signing_callback is not None
|
|
267
|
+
assert adapter.strategy_wallet_address == wallet["address"]
|
|
File without changes
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from wayfinder_paths.core.config import get_api_base_url, get_api_key
|
|
9
|
+
from wayfinder_paths.core.constants.base import DEFAULT_HTTP_TIMEOUT
|
|
10
|
+
from wayfinder_paths.mcp.utils import err, find_wallet_by_label, normalize_address, ok
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _BalanceClient:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.api_base_url = get_api_base_url()
|
|
16
|
+
timeout = httpx.Timeout(DEFAULT_HTTP_TIMEOUT)
|
|
17
|
+
self.client = httpx.AsyncClient(timeout=timeout)
|
|
18
|
+
self._headers = {"Content-Type": "application/json"}
|
|
19
|
+
api_key = get_api_key()
|
|
20
|
+
if api_key:
|
|
21
|
+
self._headers["X-API-KEY"] = api_key
|
|
22
|
+
|
|
23
|
+
async def close(self):
|
|
24
|
+
await self.client.aclose()
|
|
25
|
+
|
|
26
|
+
async def get_enriched_wallet_balances(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
wallet_address: str,
|
|
30
|
+
exclude_spam_tokens: bool = True,
|
|
31
|
+
) -> dict:
|
|
32
|
+
url = f"{self.api_base_url}/blockchain/balances/enriched/"
|
|
33
|
+
params = {
|
|
34
|
+
"address": wallet_address,
|
|
35
|
+
"exclude_spam_tokens": str(exclude_spam_tokens).lower(),
|
|
36
|
+
}
|
|
37
|
+
response = await self.client.get(url, params=params, headers=self._headers)
|
|
38
|
+
response.raise_for_status()
|
|
39
|
+
return response.json()
|
|
40
|
+
|
|
41
|
+
async def get_wallet_activity(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
wallet_address: str,
|
|
45
|
+
limit: int = 20,
|
|
46
|
+
offset: str | None = None,
|
|
47
|
+
) -> dict:
|
|
48
|
+
url = f"{self.api_base_url}/blockchain/balances/activity/"
|
|
49
|
+
params: dict[str, str | int] = {"address": wallet_address, "limit": limit}
|
|
50
|
+
if offset:
|
|
51
|
+
params["offset"] = offset
|
|
52
|
+
response = await self.client.get(url, params=params, headers=self._headers)
|
|
53
|
+
response.raise_for_status()
|
|
54
|
+
return response.json()
|
|
55
|
+
|
|
56
|
+
async def get_token_balance(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
wallet_address: str,
|
|
60
|
+
token_id: str,
|
|
61
|
+
human_readable: bool = True,
|
|
62
|
+
) -> dict:
|
|
63
|
+
url = f"{self.api_base_url}/public/balances/token/"
|
|
64
|
+
params = {
|
|
65
|
+
"wallet_address": wallet_address,
|
|
66
|
+
"token_id": token_id,
|
|
67
|
+
"human_readable": str(human_readable).lower(),
|
|
68
|
+
}
|
|
69
|
+
response = await self.client.get(url, params=params, headers=self._headers)
|
|
70
|
+
response.raise_for_status()
|
|
71
|
+
return response.json()
|
|
72
|
+
|
|
73
|
+
async def get_pool_balance(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
pool_address: str,
|
|
77
|
+
chain_id: int,
|
|
78
|
+
user_address: str,
|
|
79
|
+
human_readable: bool = True,
|
|
80
|
+
) -> dict:
|
|
81
|
+
url = f"{self.api_base_url}/public/balances/pool/"
|
|
82
|
+
params = {
|
|
83
|
+
"pool_address": pool_address,
|
|
84
|
+
"chain_id": chain_id,
|
|
85
|
+
"user_address": user_address,
|
|
86
|
+
"human_readable": str(human_readable).lower(),
|
|
87
|
+
}
|
|
88
|
+
response = await self.client.get(url, params=params, headers=self._headers)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
return response.json()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _dedupe_ordered(items: list[str]) -> list[str]:
|
|
94
|
+
seen: set[str] = set()
|
|
95
|
+
out: list[str] = []
|
|
96
|
+
for item in items:
|
|
97
|
+
key = item.lower()
|
|
98
|
+
if key in seen:
|
|
99
|
+
continue
|
|
100
|
+
seen.add(key)
|
|
101
|
+
out.append(item)
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _is_evm_address(addr: str) -> bool:
|
|
106
|
+
a = (addr or "").strip()
|
|
107
|
+
return a.startswith("0x") and len(a) == 42
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _balance_usd(entry: dict[str, Any]) -> float:
|
|
111
|
+
val = entry.get("balanceUSD", 0)
|
|
112
|
+
try:
|
|
113
|
+
return float(val or 0)
|
|
114
|
+
except (TypeError, ValueError):
|
|
115
|
+
return 0.0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def balances(
|
|
119
|
+
action: Literal["token", "pool", "enriched", "activity"],
|
|
120
|
+
*,
|
|
121
|
+
wallet_label: str | None = None,
|
|
122
|
+
wallet_address: str | None = None,
|
|
123
|
+
human_readable: bool = True,
|
|
124
|
+
include_solana: bool = False,
|
|
125
|
+
exclude_spam_tokens: bool = True,
|
|
126
|
+
limit: int = 20,
|
|
127
|
+
offset: str | None = None,
|
|
128
|
+
token_id: str | None = None,
|
|
129
|
+
token_ids: list[str] | None = None,
|
|
130
|
+
chain_id: int | None = None,
|
|
131
|
+
pool_address: str | None = None,
|
|
132
|
+
pool_addresses: list[str] | None = None,
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
waddr = normalize_address(wallet_address)
|
|
135
|
+
if not waddr:
|
|
136
|
+
want = (wallet_label or "").strip()
|
|
137
|
+
if want:
|
|
138
|
+
w = find_wallet_by_label(want)
|
|
139
|
+
if not w:
|
|
140
|
+
return err("not_found", f"Unknown wallet_label: {want}")
|
|
141
|
+
waddr = normalize_address(w.get("address"))
|
|
142
|
+
|
|
143
|
+
if not waddr:
|
|
144
|
+
return err(
|
|
145
|
+
"invalid_request", "wallet_label or wallet_address is required for balances"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
client = _BalanceClient()
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
if action == "enriched":
|
|
152
|
+
try:
|
|
153
|
+
data = await client.get_enriched_wallet_balances(
|
|
154
|
+
wallet_address=waddr,
|
|
155
|
+
exclude_spam_tokens=bool(exclude_spam_tokens),
|
|
156
|
+
)
|
|
157
|
+
if (
|
|
158
|
+
not include_solana
|
|
159
|
+
and _is_evm_address(waddr)
|
|
160
|
+
and isinstance(data, dict)
|
|
161
|
+
and isinstance(data.get("balances"), list)
|
|
162
|
+
):
|
|
163
|
+
balances_list = [b for b in data["balances"] if isinstance(b, dict)]
|
|
164
|
+
filtered = [
|
|
165
|
+
b
|
|
166
|
+
for b in balances_list
|
|
167
|
+
if str(b.get("network", "")).lower() != "solana"
|
|
168
|
+
]
|
|
169
|
+
if len(filtered) != len(balances_list):
|
|
170
|
+
out = dict(data)
|
|
171
|
+
out["balances"] = filtered
|
|
172
|
+
out["total_balance_usd"] = sum(
|
|
173
|
+
_balance_usd(b) for b in filtered
|
|
174
|
+
)
|
|
175
|
+
breakdown: dict[str, float] = {}
|
|
176
|
+
for b in filtered:
|
|
177
|
+
net = str(b.get("network") or "").strip()
|
|
178
|
+
if not net:
|
|
179
|
+
continue
|
|
180
|
+
breakdown[net] = breakdown.get(net, 0.0) + _balance_usd(b)
|
|
181
|
+
out["chain_breakdown"] = breakdown
|
|
182
|
+
out["filtered"] = {
|
|
183
|
+
"excluded_networks": ["solana"],
|
|
184
|
+
"original_count": len(balances_list),
|
|
185
|
+
"filtered_count": len(filtered),
|
|
186
|
+
}
|
|
187
|
+
data = out
|
|
188
|
+
|
|
189
|
+
return ok(data)
|
|
190
|
+
except Exception as exc: # noqa: BLE001
|
|
191
|
+
return err("balance_error", str(exc))
|
|
192
|
+
|
|
193
|
+
if action == "activity":
|
|
194
|
+
try:
|
|
195
|
+
data = await client.get_wallet_activity(
|
|
196
|
+
wallet_address=waddr,
|
|
197
|
+
limit=int(limit),
|
|
198
|
+
offset=offset,
|
|
199
|
+
)
|
|
200
|
+
return ok(
|
|
201
|
+
{
|
|
202
|
+
"wallet_address": waddr,
|
|
203
|
+
"activity": data.get("activity", []),
|
|
204
|
+
"next_offset": data.get("next_offset"),
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
except Exception as exc: # noqa: BLE001
|
|
208
|
+
return err("activity_error", str(exc))
|
|
209
|
+
|
|
210
|
+
if action == "token":
|
|
211
|
+
raw_ids: list[str] = []
|
|
212
|
+
if token_id:
|
|
213
|
+
raw_ids.append(str(token_id).strip())
|
|
214
|
+
if token_ids:
|
|
215
|
+
raw_ids.extend(str(x).strip() for x in token_ids)
|
|
216
|
+
|
|
217
|
+
ids = _dedupe_ordered([x for x in raw_ids if x])
|
|
218
|
+
if not ids:
|
|
219
|
+
return err(
|
|
220
|
+
"invalid_request",
|
|
221
|
+
"token_id or token_ids is required for balances(action=token)",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def _one(tid: str) -> dict[str, Any]:
|
|
225
|
+
try:
|
|
226
|
+
data = await client.get_token_balance(
|
|
227
|
+
token_id=tid,
|
|
228
|
+
wallet_address=waddr,
|
|
229
|
+
human_readable=bool(human_readable),
|
|
230
|
+
)
|
|
231
|
+
return {"ok": True, "token_id": tid, "data": data}
|
|
232
|
+
except Exception as exc: # noqa: BLE001
|
|
233
|
+
return {"ok": False, "token_id": tid, "error": str(exc)}
|
|
234
|
+
|
|
235
|
+
results = await asyncio.gather(*[_one(tid) for tid in ids])
|
|
236
|
+
return ok({"wallet_address": waddr, "balances": results})
|
|
237
|
+
|
|
238
|
+
if action == "pool":
|
|
239
|
+
if chain_id is None:
|
|
240
|
+
return err(
|
|
241
|
+
"invalid_request", "chain_id is required for balances(action=pool)"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
raw_pools: list[str] = []
|
|
245
|
+
if pool_address:
|
|
246
|
+
raw_pools.append(str(pool_address).strip())
|
|
247
|
+
if pool_addresses:
|
|
248
|
+
raw_pools.extend(str(x).strip() for x in pool_addresses)
|
|
249
|
+
|
|
250
|
+
pools = _dedupe_ordered([x for x in raw_pools if x])
|
|
251
|
+
if not pools:
|
|
252
|
+
return err(
|
|
253
|
+
"invalid_request",
|
|
254
|
+
"pool_address or pool_addresses is required for balances(action=pool)",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
async def _one_pool(paddr: str) -> dict[str, Any]:
|
|
258
|
+
try:
|
|
259
|
+
data = await client.get_pool_balance(
|
|
260
|
+
pool_address=paddr,
|
|
261
|
+
chain_id=int(chain_id), # type: ignore[arg-type]
|
|
262
|
+
user_address=waddr,
|
|
263
|
+
human_readable=bool(human_readable),
|
|
264
|
+
)
|
|
265
|
+
return {
|
|
266
|
+
"ok": True,
|
|
267
|
+
"pool_address": paddr,
|
|
268
|
+
"chain_id": int(chain_id), # type: ignore[arg-type]
|
|
269
|
+
"data": data,
|
|
270
|
+
}
|
|
271
|
+
except Exception as exc: # noqa: BLE001
|
|
272
|
+
return {
|
|
273
|
+
"ok": False,
|
|
274
|
+
"pool_address": paddr,
|
|
275
|
+
"chain_id": int(chain_id), # type: ignore[arg-type]
|
|
276
|
+
"error": str(exc),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
results = await asyncio.gather(*[_one_pool(p) for p in pools])
|
|
280
|
+
return ok(
|
|
281
|
+
{"wallet_address": waddr, "chain_id": int(chain_id), "pools": results}
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return err("invalid_request", f"Unknown balances action: {action}")
|
|
285
|
+
|
|
286
|
+
finally:
|
|
287
|
+
try:
|
|
288
|
+
await client.close()
|
|
289
|
+
except Exception: # noqa: BLE001
|
|
290
|
+
pass
|