wayfinder-paths 0.1.1__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 (115) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +394 -0
  2. wayfinder_paths/__init__.py +21 -0
  3. wayfinder_paths/config.example.json +20 -0
  4. wayfinder_paths/conftest.py +31 -0
  5. wayfinder_paths/core/__init__.py +13 -0
  6. wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
  7. wayfinder_paths/core/adapters/__init__.py +5 -0
  8. wayfinder_paths/core/adapters/base.py +5 -0
  9. wayfinder_paths/core/clients/AuthClient.py +83 -0
  10. wayfinder_paths/core/clients/BRAPClient.py +90 -0
  11. wayfinder_paths/core/clients/ClientManager.py +231 -0
  12. wayfinder_paths/core/clients/HyperlendClient.py +151 -0
  13. wayfinder_paths/core/clients/LedgerClient.py +222 -0
  14. wayfinder_paths/core/clients/PoolClient.py +96 -0
  15. wayfinder_paths/core/clients/SimulationClient.py +180 -0
  16. wayfinder_paths/core/clients/TokenClient.py +73 -0
  17. wayfinder_paths/core/clients/TransactionClient.py +47 -0
  18. wayfinder_paths/core/clients/WalletClient.py +90 -0
  19. wayfinder_paths/core/clients/WayfinderClient.py +258 -0
  20. wayfinder_paths/core/clients/__init__.py +48 -0
  21. wayfinder_paths/core/clients/protocols.py +295 -0
  22. wayfinder_paths/core/clients/sdk_example.py +115 -0
  23. wayfinder_paths/core/config.py +369 -0
  24. wayfinder_paths/core/constants/__init__.py +26 -0
  25. wayfinder_paths/core/constants/base.py +25 -0
  26. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  27. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  28. wayfinder_paths/core/engine/VaultJob.py +182 -0
  29. wayfinder_paths/core/engine/__init__.py +5 -0
  30. wayfinder_paths/core/engine/manifest.py +97 -0
  31. wayfinder_paths/core/services/__init__.py +0 -0
  32. wayfinder_paths/core/services/base.py +177 -0
  33. wayfinder_paths/core/services/local_evm_txn.py +429 -0
  34. wayfinder_paths/core/services/local_token_txn.py +231 -0
  35. wayfinder_paths/core/services/web3_service.py +45 -0
  36. wayfinder_paths/core/settings.py +61 -0
  37. wayfinder_paths/core/strategies/Strategy.py +183 -0
  38. wayfinder_paths/core/strategies/__init__.py +5 -0
  39. wayfinder_paths/core/strategies/base.py +7 -0
  40. wayfinder_paths/core/utils/__init__.py +1 -0
  41. wayfinder_paths/core/utils/evm_helpers.py +165 -0
  42. wayfinder_paths/core/utils/wallets.py +77 -0
  43. wayfinder_paths/core/wallets/README.md +91 -0
  44. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  45. wayfinder_paths/core/wallets/__init__.py +7 -0
  46. wayfinder_paths/run_strategy.py +409 -0
  47. wayfinder_paths/scripts/__init__.py +0 -0
  48. wayfinder_paths/scripts/create_strategy.py +181 -0
  49. wayfinder_paths/scripts/make_wallets.py +160 -0
  50. wayfinder_paths/scripts/validate_manifests.py +213 -0
  51. wayfinder_paths/tests/__init__.py +0 -0
  52. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  53. wayfinder_paths/tests/test_test_coverage.py +212 -0
  54. wayfinder_paths/tests/test_utils.py +64 -0
  55. wayfinder_paths/vaults/__init__.py +0 -0
  56. wayfinder_paths/vaults/adapters/__init__.py +0 -0
  57. wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
  58. wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
  59. wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
  60. wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
  61. wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
  62. wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
  63. wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
  64. wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
  65. wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
  66. wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
  67. wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
  68. wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
  69. wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
  70. wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
  71. wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
  72. wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
  73. wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
  74. wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
  75. wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
  76. wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
  77. wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
  78. wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
  79. wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
  80. wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
  81. wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
  82. wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
  83. wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
  84. wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
  85. wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
  86. wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
  87. wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
  88. wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
  89. wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
  90. wayfinder_paths/vaults/strategies/__init__.py +0 -0
  91. wayfinder_paths/vaults/strategies/config.py +85 -0
  92. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
  93. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
  94. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  95. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
  96. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
  97. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
  98. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
  99. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  100. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
  101. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
  102. wayfinder_paths/vaults/templates/adapter/README.md +105 -0
  103. wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
  104. wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
  105. wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
  106. wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
  107. wayfinder_paths/vaults/templates/strategy/README.md +152 -0
  108. wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
  109. wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
  110. wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
  111. wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
  112. wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
  113. wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
  114. wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
  115. wayfinder_paths-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,319 @@
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.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.vaults.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
+ "vault_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
43
+ }
44
+
45
+ s = HyperlendStableYieldStrategy(
46
+ config=mock_config,
47
+ main_wallet=mock_config["main_wallet"],
48
+ vault_wallet=mock_config["vault_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
+ s.balance_adapter.get_all_balances = AsyncMock(
66
+ return_value=(True, {"balances": []})
67
+ )
68
+
69
+ if hasattr(s, "token_adapter") and s.token_adapter:
70
+ default_usdt0 = {
71
+ "id": "usdt0-hyperevm",
72
+ "token_id": "usdt0-hyperevm",
73
+ "symbol": "USDT0",
74
+ "name": "USD Tether Zero",
75
+ "decimals": 6,
76
+ "address": "0x1234567890123456789012345678901234567890",
77
+ "chain": {"code": "hyperevm", "id": 9999, "name": "HyperEVM"},
78
+ }
79
+
80
+ default_hype = {
81
+ "id": "hype-hyperevm",
82
+ "token_id": "hype-hyperevm",
83
+ "symbol": "HYPE",
84
+ "name": "HyperEVM Gas Token",
85
+ "decimals": 18,
86
+ "address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
87
+ "chain": {"code": "hyperevm", "id": 9999, "name": "HyperEVM"},
88
+ }
89
+
90
+ def get_token_side_effect(address=None, token_id=None, **kwargs):
91
+ if token_id == "usdt0-hyperevm" or token_id == "usdt0":
92
+ return (True, default_usdt0)
93
+ elif token_id == "hype-hyperevm" or token_id == "hype":
94
+ return (True, default_hype)
95
+ return (True, default_usdt0)
96
+
97
+ s.token_adapter.get_token = AsyncMock(side_effect=get_token_side_effect)
98
+ s.token_adapter.get_token = AsyncMock(side_effect=get_token_side_effect)
99
+ s.token_adapter.get_gas_token = AsyncMock(
100
+ return_value=(
101
+ True,
102
+ default_hype,
103
+ )
104
+ )
105
+
106
+ if hasattr(s, "balance_adapter") and s.balance_adapter:
107
+ s.balance_adapter.move_from_main_wallet_to_vault_wallet = AsyncMock(
108
+ return_value=(True, "Transfer successful (simulated)")
109
+ )
110
+ s.balance_adapter.move_from_vault_wallet_to_main_wallet = AsyncMock(
111
+ return_value=(True, "Transfer successful (simulated)")
112
+ )
113
+ if hasattr(s.balance_adapter, "wallet_provider"):
114
+ s.balance_adapter.wallet_provider.broadcast_transaction = AsyncMock(
115
+ return_value=(True, {"transaction_hash": "0xCAFEBABE"})
116
+ )
117
+
118
+ if hasattr(s, "ledger_adapter") and s.ledger_adapter:
119
+ s.ledger_adapter.get_vault_net_deposit = AsyncMock(
120
+ return_value=(True, {"net_deposit": 0})
121
+ )
122
+ s.ledger_adapter.get_vault_transactions = AsyncMock(
123
+ return_value=(True, {"transactions": []})
124
+ )
125
+
126
+ if hasattr(s, "brap_adapter") and s.brap_adapter:
127
+ usdt0_address = "0x1234567890123456789012345678901234567890"
128
+
129
+ def get_swap_quote_side_effect(*args, **kwargs):
130
+ to_token_address = kwargs.get("to_token_address", "")
131
+ if to_token_address == usdt0_address:
132
+ return (
133
+ True,
134
+ {
135
+ "quotes": {
136
+ "best_quote": {
137
+ "output_amount": "99900000",
138
+ }
139
+ }
140
+ },
141
+ )
142
+ return (
143
+ True,
144
+ {
145
+ "quotes": {
146
+ "best_quote": {
147
+ "output_amount": "105000000",
148
+ "input_amount": "50000000000000",
149
+ "toAmount": "105000000",
150
+ "estimatedGas": "1000000000",
151
+ "fromAmount": "100000000",
152
+ "fromToken": {"symbol": "USDT0"},
153
+ "toToken": {"symbol": "HYPE"},
154
+ }
155
+ }
156
+ },
157
+ )
158
+
159
+ s.brap_adapter.get_swap_quote = AsyncMock(
160
+ side_effect=get_swap_quote_side_effect
161
+ )
162
+
163
+ if (
164
+ hasattr(s, "brap_adapter")
165
+ and s.brap_adapter
166
+ and hasattr(s.brap_adapter, "swap_from_quote")
167
+ ):
168
+ s.brap_adapter.swap_from_quote = AsyncMock(return_value=None)
169
+ if hasattr(s, "brap_adapter") and hasattr(s.brap_adapter, "wallet_provider"):
170
+ s.brap_adapter.wallet_provider.broadcast_transaction = AsyncMock(
171
+ return_value=(True, {"transaction_hash": "0xF00D"})
172
+ )
173
+
174
+ if hasattr(s, "hyperlend_adapter") and s.hyperlend_adapter:
175
+ s.hyperlend_adapter.get_assets_view = AsyncMock(
176
+ return_value=(True, {"assets_view": {"assets": []}})
177
+ )
178
+
179
+ s.usdt_token_info = {
180
+ "id": "usdt0-hyperevm",
181
+ "symbol": "USDT0",
182
+ "name": "USD Tether Zero",
183
+ "decimals": 6,
184
+ "address": "0x1234567890123456789012345678901234567890",
185
+ "chain": {"code": "hyperevm", "id": 9999, "name": "HyperEVM"},
186
+ }
187
+ s.hype_token_info = {
188
+ "id": "hype-hyperevm",
189
+ "symbol": "HYPE",
190
+ "name": "HyperEVM Gas Token",
191
+ "decimals": 18,
192
+ "address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
193
+ "chain": {"code": "hyperevm", "id": 9999, "name": "HyperEVM"},
194
+ }
195
+ s.current_token = None
196
+
197
+ if hasattr(s, "token_adapter") and s.token_adapter:
198
+ if not hasattr(s.token_adapter, "get_token_price"):
199
+ s.token_adapter.get_token_price = AsyncMock()
200
+
201
+ def get_token_price_side_effect(token_id):
202
+ if token_id == "hype-hyperevm":
203
+ return (True, {"current_price": 2000.0})
204
+ else:
205
+ return (True, {"current_price": 1.0})
206
+
207
+ s.token_adapter.get_token_price = AsyncMock(
208
+ side_effect=get_token_price_side_effect
209
+ )
210
+
211
+ async def mock_sweep_wallet(target_token):
212
+ pass
213
+
214
+ async def mock_refresh_current_pool_balance():
215
+ pass
216
+
217
+ async def mock_rebalance_gas(target_pool):
218
+ return (True, "Gas rebalanced")
219
+
220
+ async def mock_has_idle_assets(balances, target):
221
+ return True
222
+
223
+ if hasattr(s, "_sweep_wallet"):
224
+ s._sweep_wallet = mock_sweep_wallet
225
+ if hasattr(s, "_refresh_current_pool_balance"):
226
+ s._refresh_current_pool_balance = mock_refresh_current_pool_balance
227
+ if hasattr(s, "_rebalance_gas"):
228
+ s._rebalance_gas = mock_rebalance_gas
229
+ if hasattr(s, "_has_idle_assets"):
230
+ s._has_idle_assets = mock_has_idle_assets
231
+
232
+ s.current_symbol = getattr(s, "current_symbol", None) or "USDT0"
233
+ if not getattr(s, "current_token", None):
234
+ s.current_token = s.usdt_token_info
235
+ s.current_avg_apy = getattr(s, "current_avg_apy", 0.0)
236
+
237
+ return s
238
+
239
+
240
+ @pytest.mark.asyncio
241
+ @pytest.mark.smoke
242
+ async def test_smoke(strategy):
243
+ """REQUIRED: Basic smoke test - verifies strategy lifecycle."""
244
+ examples = load_strategy_examples(Path(__file__))
245
+ smoke_data = examples["smoke"]
246
+
247
+ st = await strategy.status()
248
+ assert isinstance(st, dict)
249
+ assert "portfolio_value" in st or "net_deposit" in st or "strategy_status" in st
250
+
251
+ deposit_params = smoke_data.get("deposit", {})
252
+ ok, msg = await strategy.deposit(**deposit_params)
253
+ assert isinstance(ok, bool)
254
+ assert isinstance(msg, str)
255
+
256
+ ok, msg = await strategy.update(**smoke_data.get("update", {}))
257
+ assert isinstance(ok, bool)
258
+
259
+ ok, msg = await strategy.withdraw(**smoke_data.get("withdraw", {}))
260
+ assert isinstance(ok, bool)
261
+
262
+
263
+ @pytest.mark.asyncio
264
+ async def test_canonical_usage(strategy):
265
+ """REQUIRED: Test canonical usage examples from examples.json (minimum).
266
+
267
+ Canonical usage = all positive usage examples (excluding error cases).
268
+ This is the MINIMUM requirement - feel free to add more test cases here.
269
+ """
270
+ examples = load_strategy_examples(Path(__file__))
271
+ canonical = get_canonical_examples(examples)
272
+
273
+ for example_name, example_data in canonical.items():
274
+ if "deposit" in example_data:
275
+ deposit_params = example_data.get("deposit", {})
276
+ ok, _ = await strategy.deposit(**deposit_params)
277
+ assert ok, f"Canonical example '{example_name}' deposit failed"
278
+
279
+ if "update" in example_data:
280
+ ok, msg = await strategy.update()
281
+ assert ok, f"Canonical example '{example_name}' update failed: {msg}"
282
+
283
+ if "status" in example_data:
284
+ st = await strategy.status()
285
+ assert isinstance(st, dict), (
286
+ f"Canonical example '{example_name}' status failed"
287
+ )
288
+
289
+
290
+ @pytest.mark.asyncio
291
+ async def test_error_cases(strategy):
292
+ """OPTIONAL: Test error scenarios from examples.json."""
293
+ examples = load_strategy_examples(Path(__file__))
294
+
295
+ for example_name, example_data in examples.items():
296
+ if isinstance(example_data, dict) and "expect" in example_data:
297
+ expect = example_data.get("expect", {})
298
+
299
+ if "deposit" in example_data:
300
+ deposit_params = example_data.get("deposit", {})
301
+ ok, _ = await strategy.deposit(**deposit_params)
302
+
303
+ if expect.get("success") is False:
304
+ assert ok is False, (
305
+ f"Expected {example_name} deposit to fail but it succeeded"
306
+ )
307
+ elif expect.get("success") is True:
308
+ assert ok is True, (
309
+ f"Expected {example_name} deposit to succeed but it failed"
310
+ )
311
+
312
+ if "update" in example_data:
313
+ ok, _ = await strategy.update()
314
+ if "success" in expect:
315
+ expected_success = expect.get("success")
316
+ assert ok == expected_success, (
317
+ f"Expected {example_name} update to "
318
+ f"{'succeed' if expected_success else 'fail'} but got opposite"
319
+ )
@@ -0,0 +1,95 @@
1
+ # Stablecoin Yield Strategy
2
+
3
+ - Entrypoint: `vaults.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 vault 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 vault 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 vault 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 vault 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 vault wallet (when requested or when the vault needs a top-up), then transfers the requested USDC amount via `BalanceAdapter.move_from_main_wallet_to_vault_wallet`.
45
+ - Hydrates the on-chain position snapshot so future updates know which pool is active.
46
+
47
+ ### Update
48
+
49
+ - Fetches the latest vault 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_vault_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 vault 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_vault_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 vault wallet to the main wallet via `BalanceAdapter.move_from_vault_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 default + vault wallets (writes wallets.json)
76
+ poetry run python wayfinder_paths/scripts/make_wallets.py --default --vault
77
+
78
+ # Copy the example config and set credentials if needed
79
+ cp wayfinder_paths/config.example.json config.json
80
+
81
+ # Smoke test the strategy
82
+ poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --action status --config $(pwd)/config.json
83
+
84
+ # Perform a funded deposit/update cycle
85
+ 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
86
+ poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --action update --config $(pwd)/config.json
87
+ ```
88
+
89
+ You can also load the manifest explicitly:
90
+
91
+ ```bash
92
+ poetry run python wayfinder_paths/run_strategy.py --manifest wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml --action status --config $(pwd)/config.json
93
+ ```
94
+
95
+ 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: "vaults.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", "vault.transactions"]
16
+ - name: "EVM_TRANSACTION"
17
+ capabilities: ["wallet_transfer"]