wayfinder-paths 0.1.7__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/CONFIG_GUIDE.md +399 -0
- wayfinder_paths/__init__.py +22 -0
- wayfinder_paths/abis/generic/erc20.json +383 -0
- wayfinder_paths/adapters/__init__.py +0 -0
- wayfinder_paths/adapters/balance_adapter/README.md +94 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
- wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
- wayfinder_paths/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
- wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
- wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
- wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
- wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
- wayfinder_paths/adapters/pool_adapter/README.md +206 -0
- wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
- wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
- wayfinder_paths/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
- wayfinder_paths/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
- wayfinder_paths/config.example.json +22 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +18 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/adapters/models.py +46 -0
- wayfinder_paths/core/analytics/__init__.py +11 -0
- wayfinder_paths/core/analytics/bootstrap.py +57 -0
- wayfinder_paths/core/analytics/stats.py +48 -0
- wayfinder_paths/core/analytics/test_analytics.py +170 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +109 -0
- wayfinder_paths/core/clients/ClientManager.py +210 -0
- wayfinder_paths/core/clients/HyperlendClient.py +192 -0
- wayfinder_paths/core/clients/LedgerClient.py +443 -0
- wayfinder_paths/core/clients/PoolClient.py +128 -0
- wayfinder_paths/core/clients/SimulationClient.py +192 -0
- wayfinder_paths/core/clients/TokenClient.py +89 -0
- wayfinder_paths/core/clients/TransactionClient.py +63 -0
- wayfinder_paths/core/clients/WalletClient.py +94 -0
- wayfinder_paths/core/clients/WayfinderClient.py +269 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +392 -0
- wayfinder_paths/core/clients/sdk_example.py +110 -0
- wayfinder_paths/core/config.py +458 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +42 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/StrategyJob.py +188 -0
- wayfinder_paths/core/engine/__init__.py +5 -0
- wayfinder_paths/core/engine/manifest.py +97 -0
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +179 -0
- wayfinder_paths/core/services/local_evm_txn.py +430 -0
- wayfinder_paths/core/services/local_token_txn.py +231 -0
- wayfinder_paths/core/services/web3_service.py +45 -0
- wayfinder_paths/core/settings.py +61 -0
- wayfinder_paths/core/strategies/Strategy.py +280 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/strategies/descriptors.py +81 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +206 -0
- wayfinder_paths/core/utils/wallets.py +77 -0
- wayfinder_paths/core/wallets/README.md +91 -0
- wayfinder_paths/core/wallets/WalletManager.py +56 -0
- wayfinder_paths/core/wallets/__init__.py +7 -0
- wayfinder_paths/policies/enso.py +17 -0
- wayfinder_paths/policies/erc20.py +34 -0
- wayfinder_paths/policies/evm.py +21 -0
- wayfinder_paths/policies/hyper_evm.py +19 -0
- wayfinder_paths/policies/hyperlend.py +12 -0
- wayfinder_paths/policies/hyperliquid.py +30 -0
- wayfinder_paths/policies/moonwell.py +54 -0
- wayfinder_paths/policies/prjx.py +30 -0
- wayfinder_paths/policies/util.py +27 -0
- wayfinder_paths/run_strategy.py +411 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +169 -0
- wayfinder_paths/scripts/run_strategy.py +124 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/strategies/__init__.py +0 -0
- wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
- wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
- wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
- wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
- wayfinder_paths/strategies/config.py +85 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
- wayfinder_paths/templates/adapter/README.md +105 -0
- wayfinder_paths/templates/adapter/adapter.py +26 -0
- wayfinder_paths/templates/adapter/examples.json +8 -0
- wayfinder_paths/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/templates/strategy/README.md +153 -0
- wayfinder_paths/templates/strategy/examples.json +11 -0
- wayfinder_paths/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/templates/strategy/strategy.py +57 -0
- wayfinder_paths/templates/strategy/test_strategy.py +197 -0
- wayfinder_paths/tests/__init__.py +0 -0
- wayfinder_paths/tests/test_smoke_manifest.py +48 -0
- wayfinder_paths/tests/test_test_coverage.py +212 -0
- wayfinder_paths/tests/test_utils.py +64 -0
- wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
- wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
- wayfinder_paths-0.1.7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from unittest.mock import AsyncMock
|
|
4
|
+
|
|
5
|
+
# Ensure wayfinder-paths is on path for tests.test_utils import
|
|
6
|
+
# This is a workaround until conftest loading order is resolved
|
|
7
|
+
_wayfinder_path_dir = Path(__file__).parent.parent.parent.resolve()
|
|
8
|
+
_wayfinder_path_str = str(_wayfinder_path_dir)
|
|
9
|
+
if _wayfinder_path_str not in sys.path:
|
|
10
|
+
sys.path.insert(0, _wayfinder_path_str)
|
|
11
|
+
elif sys.path.index(_wayfinder_path_str) > 0:
|
|
12
|
+
# Move to front to take precedence
|
|
13
|
+
sys.path.remove(_wayfinder_path_str)
|
|
14
|
+
sys.path.insert(0, _wayfinder_path_str)
|
|
15
|
+
|
|
16
|
+
import pytest # noqa: E402
|
|
17
|
+
|
|
18
|
+
# Import test utilities
|
|
19
|
+
try:
|
|
20
|
+
from tests.test_utils import get_canonical_examples, load_strategy_examples
|
|
21
|
+
except ImportError:
|
|
22
|
+
# Fallback if path setup didn't work
|
|
23
|
+
import importlib.util
|
|
24
|
+
|
|
25
|
+
test_utils_path = Path(_wayfinder_path_dir) / "tests" / "test_utils.py"
|
|
26
|
+
spec = importlib.util.spec_from_file_location("tests.test_utils", test_utils_path)
|
|
27
|
+
test_utils = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(test_utils)
|
|
29
|
+
get_canonical_examples = test_utils.get_canonical_examples
|
|
30
|
+
load_strategy_examples = test_utils.load_strategy_examples
|
|
31
|
+
|
|
32
|
+
from wayfinder_paths.strategies.hyperlend_stable_yield_strategy.strategy import ( # noqa: E402
|
|
33
|
+
HyperlendStableYieldStrategy,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def strategy():
|
|
39
|
+
"""Create a strategy instance for testing with minimal config."""
|
|
40
|
+
mock_config = {
|
|
41
|
+
"main_wallet": {"address": "0x1234567890123456789012345678901234567890"},
|
|
42
|
+
"strategy_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
s = HyperlendStableYieldStrategy(
|
|
46
|
+
config=mock_config,
|
|
47
|
+
main_wallet=mock_config["main_wallet"],
|
|
48
|
+
strategy_wallet=mock_config["strategy_wallet"],
|
|
49
|
+
simulation=True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if hasattr(s, "balance_adapter") and s.balance_adapter:
|
|
53
|
+
# Mock balances: 1000 USDT0 (with 6 decimals) and 2 HYPE (with 18 decimals)
|
|
54
|
+
usdt0_balance_mock = AsyncMock(return_value=(True, 1000000000))
|
|
55
|
+
hype_balance_mock = AsyncMock(return_value=(True, 2000000000000000000))
|
|
56
|
+
|
|
57
|
+
def get_balance_side_effect(token_id, wallet_address, **kwargs):
|
|
58
|
+
if token_id == "usdt0-hyperevm" or token_id == "usdt0":
|
|
59
|
+
return usdt0_balance_mock.return_value
|
|
60
|
+
elif token_id == "hype-hyperevm" or token_id == "hype":
|
|
61
|
+
return hype_balance_mock.return_value
|
|
62
|
+
return (True, 1000000000)
|
|
63
|
+
|
|
64
|
+
s.balance_adapter.get_balance = AsyncMock(side_effect=get_balance_side_effect)
|
|
65
|
+
|
|
66
|
+
if hasattr(s, "token_adapter") and s.token_adapter:
|
|
67
|
+
default_usdt0 = {
|
|
68
|
+
"id": "usdt0-hyperevm",
|
|
69
|
+
"token_id": "usdt0-hyperevm",
|
|
70
|
+
"symbol": "USDT0",
|
|
71
|
+
"name": "USD Tether Zero",
|
|
72
|
+
"decimals": 6,
|
|
73
|
+
"address": "0x1234567890123456789012345678901234567890",
|
|
74
|
+
"chain": {"code": "hyperevm", "id": 9999, "name": "HyperEVM"},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
default_hype = {
|
|
78
|
+
"id": "hype-hyperevm",
|
|
79
|
+
"token_id": "hype-hyperevm",
|
|
80
|
+
"symbol": "HYPE",
|
|
81
|
+
"name": "HyperEVM Gas Token",
|
|
82
|
+
"decimals": 18,
|
|
83
|
+
"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
|
|
84
|
+
"chain": {"code": "hyperevm", "id": 9999, "name": "HyperEVM"},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def get_token_side_effect(address=None, token_id=None, **kwargs):
|
|
88
|
+
if token_id == "usdt0-hyperevm" or token_id == "usdt0":
|
|
89
|
+
return (True, default_usdt0)
|
|
90
|
+
elif token_id == "hype-hyperevm" or token_id == "hype":
|
|
91
|
+
return (True, default_hype)
|
|
92
|
+
return (True, default_usdt0)
|
|
93
|
+
|
|
94
|
+
s.token_adapter.get_token = AsyncMock(side_effect=get_token_side_effect)
|
|
95
|
+
s.token_adapter.get_token = AsyncMock(side_effect=get_token_side_effect)
|
|
96
|
+
s.token_adapter.get_gas_token = AsyncMock(
|
|
97
|
+
return_value=(
|
|
98
|
+
True,
|
|
99
|
+
default_hype,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if hasattr(s, "balance_adapter") and s.balance_adapter:
|
|
104
|
+
s.balance_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
|
|
105
|
+
return_value=(True, "Transfer successful (simulated)")
|
|
106
|
+
)
|
|
107
|
+
s.balance_adapter.move_from_strategy_wallet_to_main_wallet = AsyncMock(
|
|
108
|
+
return_value=(True, "Transfer successful (simulated)")
|
|
109
|
+
)
|
|
110
|
+
if hasattr(s.balance_adapter, "wallet_provider"):
|
|
111
|
+
s.balance_adapter.wallet_provider.broadcast_transaction = AsyncMock(
|
|
112
|
+
return_value=(True, {"transaction_hash": "0xCAFEBABE"})
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if hasattr(s, "ledger_adapter") and s.ledger_adapter:
|
|
116
|
+
s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
|
|
117
|
+
return_value=(True, {"net_deposit": 0})
|
|
118
|
+
)
|
|
119
|
+
s.ledger_adapter.get_strategy_transactions = AsyncMock(
|
|
120
|
+
return_value=(True, {"transactions": []})
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if hasattr(s, "brap_adapter") and s.brap_adapter:
|
|
124
|
+
usdt0_address = "0x1234567890123456789012345678901234567890"
|
|
125
|
+
|
|
126
|
+
def get_swap_quote_side_effect(*args, **kwargs):
|
|
127
|
+
to_token_address = kwargs.get("to_token_address", "")
|
|
128
|
+
if to_token_address == usdt0_address:
|
|
129
|
+
return (
|
|
130
|
+
True,
|
|
131
|
+
{
|
|
132
|
+
"quotes": {
|
|
133
|
+
"best_quote": {
|
|
134
|
+
"output_amount": "99900000",
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
return (
|
|
140
|
+
True,
|
|
141
|
+
{
|
|
142
|
+
"quotes": {
|
|
143
|
+
"best_quote": {
|
|
144
|
+
"output_amount": "105000000",
|
|
145
|
+
"input_amount": "50000000000000",
|
|
146
|
+
"toAmount": "105000000",
|
|
147
|
+
"estimatedGas": "1000000000",
|
|
148
|
+
"fromAmount": "100000000",
|
|
149
|
+
"fromToken": {"symbol": "USDT0"},
|
|
150
|
+
"toToken": {"symbol": "HYPE"},
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
s.brap_adapter.get_swap_quote = AsyncMock(
|
|
157
|
+
side_effect=get_swap_quote_side_effect
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
hasattr(s, "brap_adapter")
|
|
162
|
+
and s.brap_adapter
|
|
163
|
+
and hasattr(s.brap_adapter, "swap_from_quote")
|
|
164
|
+
):
|
|
165
|
+
s.brap_adapter.swap_from_quote = AsyncMock(return_value=None)
|
|
166
|
+
if hasattr(s, "brap_adapter") and hasattr(s.brap_adapter, "wallet_provider"):
|
|
167
|
+
s.brap_adapter.wallet_provider.broadcast_transaction = AsyncMock(
|
|
168
|
+
return_value=(True, {"transaction_hash": "0xF00D"})
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if hasattr(s, "hyperlend_adapter") and s.hyperlend_adapter:
|
|
172
|
+
s.hyperlend_adapter.get_assets_view = AsyncMock(
|
|
173
|
+
return_value=(True, {"assets_view": {"assets": []}})
|
|
174
|
+
)
|
|
175
|
+
s.hyperlend_adapter.get_stable_markets = AsyncMock(
|
|
176
|
+
return_value=(
|
|
177
|
+
True,
|
|
178
|
+
{
|
|
179
|
+
"markets": {
|
|
180
|
+
"0x1234567890123456789012345678901234567890": {
|
|
181
|
+
"symbol": "USDT0",
|
|
182
|
+
"address": "0x1234567890123456789012345678901234567890",
|
|
183
|
+
"apy": 5.0,
|
|
184
|
+
"tvl": 1000000,
|
|
185
|
+
"underlying_token": {
|
|
186
|
+
"address": "0x1234567890123456789012345678901234567890",
|
|
187
|
+
"symbol": "USDT0",
|
|
188
|
+
"decimals": 6,
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"notes": [],
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
s.hyperlend_adapter.get_lend_rate_history = AsyncMock(
|
|
197
|
+
return_value=(
|
|
198
|
+
True,
|
|
199
|
+
{
|
|
200
|
+
"rates": [{"rate": 5.0, "timestamp": 1700000000}],
|
|
201
|
+
"avg_rate": 5.0,
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
s.usdt_token_info = {
|
|
207
|
+
"id": "usdt0-hyperevm",
|
|
208
|
+
"symbol": "USDT0",
|
|
209
|
+
"name": "USD Tether Zero",
|
|
210
|
+
"decimals": 6,
|
|
211
|
+
"address": "0x1234567890123456789012345678901234567890",
|
|
212
|
+
"chain": {"code": "hyperevm", "id": 9999, "name": "HyperEVM"},
|
|
213
|
+
}
|
|
214
|
+
s.hype_token_info = {
|
|
215
|
+
"id": "hype-hyperevm",
|
|
216
|
+
"symbol": "HYPE",
|
|
217
|
+
"name": "HyperEVM Gas Token",
|
|
218
|
+
"decimals": 18,
|
|
219
|
+
"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
|
|
220
|
+
"chain": {"code": "hyperevm", "id": 9999, "name": "HyperEVM"},
|
|
221
|
+
}
|
|
222
|
+
s.current_token = None
|
|
223
|
+
|
|
224
|
+
if hasattr(s, "token_adapter") and s.token_adapter:
|
|
225
|
+
if not hasattr(s.token_adapter, "get_token_price"):
|
|
226
|
+
s.token_adapter.get_token_price = AsyncMock()
|
|
227
|
+
|
|
228
|
+
def get_token_price_side_effect(token_id):
|
|
229
|
+
if token_id == "hype-hyperevm":
|
|
230
|
+
return (True, {"current_price": 2000.0})
|
|
231
|
+
else:
|
|
232
|
+
return (True, {"current_price": 1.0})
|
|
233
|
+
|
|
234
|
+
s.token_adapter.get_token_price = AsyncMock(
|
|
235
|
+
side_effect=get_token_price_side_effect
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def mock_sweep_wallet(target_token):
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
async def mock_refresh_current_pool_balance():
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
async def mock_rebalance_gas(target_pool):
|
|
245
|
+
return (True, "Gas rebalanced")
|
|
246
|
+
|
|
247
|
+
async def mock_has_idle_assets(balances, target):
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
if hasattr(s, "_sweep_wallet"):
|
|
251
|
+
s._sweep_wallet = mock_sweep_wallet
|
|
252
|
+
if hasattr(s, "_refresh_current_pool_balance"):
|
|
253
|
+
s._refresh_current_pool_balance = mock_refresh_current_pool_balance
|
|
254
|
+
if hasattr(s, "_rebalance_gas"):
|
|
255
|
+
s._rebalance_gas = mock_rebalance_gas
|
|
256
|
+
if hasattr(s, "_has_idle_assets"):
|
|
257
|
+
s._has_idle_assets = mock_has_idle_assets
|
|
258
|
+
|
|
259
|
+
s.current_symbol = getattr(s, "current_symbol", None) or "USDT0"
|
|
260
|
+
if not getattr(s, "current_token", None):
|
|
261
|
+
s.current_token = s.usdt_token_info
|
|
262
|
+
s.current_avg_apy = getattr(s, "current_avg_apy", 0.0)
|
|
263
|
+
|
|
264
|
+
return s
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@pytest.mark.asyncio
|
|
268
|
+
@pytest.mark.smoke
|
|
269
|
+
async def test_smoke(strategy):
|
|
270
|
+
"""REQUIRED: Basic smoke test - verifies strategy lifecycle."""
|
|
271
|
+
examples = load_strategy_examples(Path(__file__))
|
|
272
|
+
smoke_data = examples["smoke"]
|
|
273
|
+
|
|
274
|
+
await strategy.setup()
|
|
275
|
+
|
|
276
|
+
st = await strategy.status()
|
|
277
|
+
assert isinstance(st, dict)
|
|
278
|
+
assert "portfolio_value" in st or "net_deposit" in st or "strategy_status" in st
|
|
279
|
+
|
|
280
|
+
deposit_params = smoke_data.get("deposit", {})
|
|
281
|
+
ok, msg = await strategy.deposit(**deposit_params)
|
|
282
|
+
assert isinstance(ok, bool)
|
|
283
|
+
assert isinstance(msg, str)
|
|
284
|
+
|
|
285
|
+
result = await strategy.update(**smoke_data.get("update", {}))
|
|
286
|
+
# update() returns (ok, msg, should_notify) or (ok, msg)
|
|
287
|
+
ok = result[0]
|
|
288
|
+
assert isinstance(ok, bool)
|
|
289
|
+
|
|
290
|
+
ok, msg = await strategy.withdraw(**smoke_data.get("withdraw", {}))
|
|
291
|
+
assert isinstance(ok, bool)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@pytest.mark.asyncio
|
|
295
|
+
async def test_canonical_usage(strategy):
|
|
296
|
+
"""REQUIRED: Test canonical usage examples from examples.json (minimum).
|
|
297
|
+
|
|
298
|
+
Canonical usage = all positive usage examples (excluding error cases).
|
|
299
|
+
This is the MINIMUM requirement - feel free to add more test cases here.
|
|
300
|
+
"""
|
|
301
|
+
examples = load_strategy_examples(Path(__file__))
|
|
302
|
+
canonical = get_canonical_examples(examples)
|
|
303
|
+
|
|
304
|
+
for example_name, example_data in canonical.items():
|
|
305
|
+
if "deposit" in example_data:
|
|
306
|
+
deposit_params = example_data.get("deposit", {})
|
|
307
|
+
ok, _ = await strategy.deposit(**deposit_params)
|
|
308
|
+
assert ok, f"Canonical example '{example_name}' deposit failed"
|
|
309
|
+
|
|
310
|
+
if "update" in example_data:
|
|
311
|
+
result = await strategy.update()
|
|
312
|
+
ok = result[0]
|
|
313
|
+
msg = result[1] if len(result) > 1 else ""
|
|
314
|
+
assert ok, f"Canonical example '{example_name}' update failed: {msg}"
|
|
315
|
+
|
|
316
|
+
if "status" in example_data:
|
|
317
|
+
st = await strategy.status()
|
|
318
|
+
assert isinstance(st, dict), (
|
|
319
|
+
f"Canonical example '{example_name}' status failed"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@pytest.mark.asyncio
|
|
324
|
+
async def test_error_cases(strategy):
|
|
325
|
+
"""OPTIONAL: Test error scenarios from examples.json."""
|
|
326
|
+
examples = load_strategy_examples(Path(__file__))
|
|
327
|
+
|
|
328
|
+
for example_name, example_data in examples.items():
|
|
329
|
+
if isinstance(example_data, dict) and "expect" in example_data:
|
|
330
|
+
expect = example_data.get("expect", {})
|
|
331
|
+
|
|
332
|
+
if "deposit" in example_data:
|
|
333
|
+
deposit_params = example_data.get("deposit", {})
|
|
334
|
+
ok, _ = await strategy.deposit(**deposit_params)
|
|
335
|
+
|
|
336
|
+
if expect.get("success") is False:
|
|
337
|
+
assert ok is False, (
|
|
338
|
+
f"Expected {example_name} deposit to fail but it succeeded"
|
|
339
|
+
)
|
|
340
|
+
elif expect.get("success") is True:
|
|
341
|
+
assert ok is True, (
|
|
342
|
+
f"Expected {example_name} deposit to succeed but it failed"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if "update" in example_data:
|
|
346
|
+
ok, _ = await strategy.update()
|
|
347
|
+
if "success" in expect:
|
|
348
|
+
expected_success = expect.get("success")
|
|
349
|
+
assert ok == expected_success, (
|
|
350
|
+
f"Expected {example_name} update to "
|
|
351
|
+
f"{'succeed' if expected_success else 'fail'} but got opposite"
|
|
352
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Stablecoin Yield Strategy
|
|
2
|
+
|
|
3
|
+
- Entrypoint: `strategies.stablecoin_yield_strategy.strategy.StablecoinYieldStrategy`
|
|
4
|
+
- Manifest: `manifest.yaml`
|
|
5
|
+
- Examples: `examples.json`
|
|
6
|
+
- Tests: `test_strategy.py`
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
Actively manages Base USDC deposits. Deposits pull USDC (plus an ETH gas buffer) from the main wallet into the strategy wallet, then the strategy searches Base-native pools for the best USD-denominated APY. Updates monitor DeFi Llama feeds and Wayfinder pool analytics, respecting a rotation cooldown and minimum APY improvement before rebalancing via the BRAP router. Withdrawals unwind the current position, sweep residual tokens back into USDC, and return funds to the main wallet.
|
|
11
|
+
|
|
12
|
+
## On-chain policy
|
|
13
|
+
|
|
14
|
+
Transactions are scoped to the strategy wallet and Enso Router approval/swap calls:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
(wallet.id == 'FORMAT_WALLET_ID') && ((eth.tx.data[0..10] == '0x095ea7b3' && eth.tx.data[34..74] == 'f75584ef6673ad213a685a1b58cc0330b8ea22cf') || (eth.tx.to == '0xF75584eF6673aD213a685a1B58Cc0330B8eA22Cf'))
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Key parameters (from `strategy.py`)
|
|
21
|
+
|
|
22
|
+
- `MIN_AMOUNT_USDC = 2` → deposits smaller than 2 USDC are rejected.
|
|
23
|
+
- `MIN_TVL = 1_000_000` → pools below $1M TVL are ignored.
|
|
24
|
+
- `ROTATION_MIN_INTERVAL = 14 days` → once rotated, the strategy waits ~2 weeks unless the new candidate dramatically outperforms.
|
|
25
|
+
- `DUST_APY = 0.01` (1%) → pools below this APY are treated as dust.
|
|
26
|
+
- `SEARCH_DEPTH = 10` → how many pools to examine when selecting candidates.
|
|
27
|
+
- `MIN_GAS = 0.001` and `GAS_MAXIMUM = 0.02` Base ETH → minimum buffer required in the strategy wallet plus the upper bound accepted per deposit.
|
|
28
|
+
|
|
29
|
+
## Adapters used
|
|
30
|
+
|
|
31
|
+
- `BalanceAdapter` for wallet/pool balances and orchestrating transfers between the main and strategy wallets (with ledger recording).
|
|
32
|
+
- `PoolAdapter` for pool metadata, llama reports, and yield analytics.
|
|
33
|
+
- `BRAPAdapter` to source swap quotes and execute rotations.
|
|
34
|
+
- `TokenAdapter` for metadata (gas token, USDC info).
|
|
35
|
+
- `LedgerAdapter` for net-deposit tracking and cooldown enforcement.
|
|
36
|
+
- `LocalTokenTxnService` (via `DefaultWeb3Service`) for lower-level sends/approvals used by adapters.
|
|
37
|
+
|
|
38
|
+
## Actions
|
|
39
|
+
|
|
40
|
+
### Deposit
|
|
41
|
+
|
|
42
|
+
- Validates `main_token_amount` ≥ `MIN_AMOUNT_USDC` and `gas_token_amount` ≤ `GAS_MAXIMUM`.
|
|
43
|
+
- Confirms the main wallet holds enough USDC and Base ETH.
|
|
44
|
+
- Moves Base ETH into the strategy wallet (when requested or when the strategy needs a top-up), then transfers the requested USDC amount via `BalanceAdapter.move_from_main_wallet_to_strategy_wallet`.
|
|
45
|
+
- Hydrates the on-chain position snapshot so future updates know which pool is active.
|
|
46
|
+
|
|
47
|
+
### Update
|
|
48
|
+
|
|
49
|
+
- Fetches the latest strategy balances, idle assets, and current target pool.
|
|
50
|
+
- Runs `_find_best_pool()` which uses `PoolAdapter` and DeFi Llama data to score up to `SEARCH_DEPTH` pools that satisfy the APY/TVL filters.
|
|
51
|
+
- Checks `LedgerAdapter.get_strategy_latest_transactions()` to enforce the rotation cooldown, unless the new candidate clears the APY-improvement threshold.
|
|
52
|
+
- If rotation is approved, requests a BRAP quote, ensures the strategy has enough gas, executes the swap via `BRAPAdapter.swap_from_quote`, and sweeps any idle balances back into the target token.
|
|
53
|
+
- Records informative status messages when no better pool exists or when cooldown blocks a move.
|
|
54
|
+
|
|
55
|
+
### Status
|
|
56
|
+
|
|
57
|
+
`_status()` reports:
|
|
58
|
+
|
|
59
|
+
- `portfolio_value`: refreshed pool balance (in base units) converted to float.
|
|
60
|
+
- `net_deposit`: data pulled from `LedgerAdapter.get_strategy_net_deposit`.
|
|
61
|
+
- `strategy_status`: dictionary exposing the active pool, APY estimates, and wallet balances.
|
|
62
|
+
|
|
63
|
+
### Withdraw
|
|
64
|
+
|
|
65
|
+
- Requires a prior deposit (the strategy tracks `self.DEPOSIT_USDC`).
|
|
66
|
+
- Reads the pool balance via `BalanceAdapter.get_pool_balance`, unwinds via BRAP swaps back to USDC, and moves USDC from the strategy wallet to the main wallet via `BalanceAdapter.move_from_strategy_wallet_to_main_wallet`.
|
|
67
|
+
- Updates the ledger and clears cached pool state.
|
|
68
|
+
|
|
69
|
+
## Running locally
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Install dependencies
|
|
73
|
+
poetry install
|
|
74
|
+
|
|
75
|
+
# Generate main wallet (writes wallets.json)
|
|
76
|
+
# Creates a main wallet (or use 'just create-strategy' which auto-creates wallets)
|
|
77
|
+
poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
|
|
78
|
+
|
|
79
|
+
# Copy the example config and set credentials if needed
|
|
80
|
+
cp wayfinder_paths/config.example.json config.json
|
|
81
|
+
|
|
82
|
+
# Smoke test the strategy
|
|
83
|
+
poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --action status --config $(pwd)/config.json
|
|
84
|
+
|
|
85
|
+
# Perform a funded deposit/update cycle
|
|
86
|
+
poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --action deposit --main-token-amount 60 --gas-token-amount 0.001 --config $(pwd)/config.json
|
|
87
|
+
poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --action update --config $(pwd)/config.json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
You can also load the manifest explicitly:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
poetry run python wayfinder_paths/run_strategy.py --manifest wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml --action status --config $(pwd)/config.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Wallet addresses are auto-populated from `wallets.json` when you run `wayfinder_paths/scripts/make_wallets.py`. Set `NETWORK=testnet` in `config.json` to dry-run operations against mocked services.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"smoke": {
|
|
3
|
+
"deposit": {"main_token_amount": 60, "gas_token_amount": 0.001},
|
|
4
|
+
"update": {},
|
|
5
|
+
"status": {},
|
|
6
|
+
"withdraw": {}
|
|
7
|
+
},
|
|
8
|
+
"min_deposit_fail": {
|
|
9
|
+
"deposit": {"main_token_amount": 1, "gas_token_amount": 0.0},
|
|
10
|
+
"expect": {"success": false, "message_contains": "Minimum deposit"}
|
|
11
|
+
},
|
|
12
|
+
"tvl_filter": {
|
|
13
|
+
"deposit": {"main_token_amount": 100, "gas_token_amount": 0.001},
|
|
14
|
+
"update": {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
schema_version: "0.1"
|
|
2
|
+
entrypoint: "strategies.stablecoin_yield_strategy.strategy.StablecoinYieldStrategy"
|
|
3
|
+
permissions:
|
|
4
|
+
policy: "(wallet.id == 'FORMAT_WALLET_ID') && ((eth.tx.data[0..10] == '0x095ea7b3' && eth.tx.data[34..74] == 'f75584ef6673ad213a685a1b58cc0330b8ea22cf') || (eth.tx.to == '0xF75584eF6673aD213a685a1B58Cc0330B8eA22Cf'))"
|
|
5
|
+
adapters:
|
|
6
|
+
- name: "BALANCE"
|
|
7
|
+
capabilities: ["wallet_read", "wallet_transfer"]
|
|
8
|
+
- name: "POOL"
|
|
9
|
+
capabilities: ["pool.read", "pool.analytics"]
|
|
10
|
+
- name: "BRAP"
|
|
11
|
+
capabilities: ["swap.quote", "swap.execute"]
|
|
12
|
+
- name: "TOKEN"
|
|
13
|
+
capabilities: ["token.read"]
|
|
14
|
+
- name: "LEDGER"
|
|
15
|
+
capabilities: ["ledger.read", "strategy.transactions"]
|
|
16
|
+
- name: "EVM_TRANSACTION"
|
|
17
|
+
capabilities: ["wallet_transfer"]
|