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.

Files changed (149) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +399 -0
  2. wayfinder_paths/__init__.py +22 -0
  3. wayfinder_paths/abis/generic/erc20.json +383 -0
  4. wayfinder_paths/adapters/__init__.py +0 -0
  5. wayfinder_paths/adapters/balance_adapter/README.md +94 -0
  6. wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
  7. wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
  8. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  9. wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
  10. wayfinder_paths/adapters/brap_adapter/README.md +249 -0
  11. wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
  12. wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
  13. wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
  15. wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
  16. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
  18. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
  19. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
  20. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
  21. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
  23. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
  24. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
  28. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
  29. wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
  30. wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
  31. wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
  32. wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
  33. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
  34. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
  35. wayfinder_paths/adapters/pool_adapter/README.md +206 -0
  36. wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
  37. wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
  38. wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
  39. wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
  40. wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
  41. wayfinder_paths/adapters/token_adapter/README.md +101 -0
  42. wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
  43. wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
  44. wayfinder_paths/adapters/token_adapter/examples.json +26 -0
  45. wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
  46. wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
  47. wayfinder_paths/config.example.json +22 -0
  48. wayfinder_paths/conftest.py +31 -0
  49. wayfinder_paths/core/__init__.py +18 -0
  50. wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
  51. wayfinder_paths/core/adapters/__init__.py +5 -0
  52. wayfinder_paths/core/adapters/base.py +5 -0
  53. wayfinder_paths/core/adapters/models.py +46 -0
  54. wayfinder_paths/core/analytics/__init__.py +11 -0
  55. wayfinder_paths/core/analytics/bootstrap.py +57 -0
  56. wayfinder_paths/core/analytics/stats.py +48 -0
  57. wayfinder_paths/core/analytics/test_analytics.py +170 -0
  58. wayfinder_paths/core/clients/AuthClient.py +83 -0
  59. wayfinder_paths/core/clients/BRAPClient.py +109 -0
  60. wayfinder_paths/core/clients/ClientManager.py +210 -0
  61. wayfinder_paths/core/clients/HyperlendClient.py +192 -0
  62. wayfinder_paths/core/clients/LedgerClient.py +443 -0
  63. wayfinder_paths/core/clients/PoolClient.py +128 -0
  64. wayfinder_paths/core/clients/SimulationClient.py +192 -0
  65. wayfinder_paths/core/clients/TokenClient.py +89 -0
  66. wayfinder_paths/core/clients/TransactionClient.py +63 -0
  67. wayfinder_paths/core/clients/WalletClient.py +94 -0
  68. wayfinder_paths/core/clients/WayfinderClient.py +269 -0
  69. wayfinder_paths/core/clients/__init__.py +48 -0
  70. wayfinder_paths/core/clients/protocols.py +392 -0
  71. wayfinder_paths/core/clients/sdk_example.py +110 -0
  72. wayfinder_paths/core/config.py +458 -0
  73. wayfinder_paths/core/constants/__init__.py +26 -0
  74. wayfinder_paths/core/constants/base.py +42 -0
  75. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  76. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  77. wayfinder_paths/core/engine/StrategyJob.py +188 -0
  78. wayfinder_paths/core/engine/__init__.py +5 -0
  79. wayfinder_paths/core/engine/manifest.py +97 -0
  80. wayfinder_paths/core/services/__init__.py +0 -0
  81. wayfinder_paths/core/services/base.py +179 -0
  82. wayfinder_paths/core/services/local_evm_txn.py +430 -0
  83. wayfinder_paths/core/services/local_token_txn.py +231 -0
  84. wayfinder_paths/core/services/web3_service.py +45 -0
  85. wayfinder_paths/core/settings.py +61 -0
  86. wayfinder_paths/core/strategies/Strategy.py +280 -0
  87. wayfinder_paths/core/strategies/__init__.py +5 -0
  88. wayfinder_paths/core/strategies/base.py +7 -0
  89. wayfinder_paths/core/strategies/descriptors.py +81 -0
  90. wayfinder_paths/core/utils/__init__.py +1 -0
  91. wayfinder_paths/core/utils/evm_helpers.py +206 -0
  92. wayfinder_paths/core/utils/wallets.py +77 -0
  93. wayfinder_paths/core/wallets/README.md +91 -0
  94. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  95. wayfinder_paths/core/wallets/__init__.py +7 -0
  96. wayfinder_paths/policies/enso.py +17 -0
  97. wayfinder_paths/policies/erc20.py +34 -0
  98. wayfinder_paths/policies/evm.py +21 -0
  99. wayfinder_paths/policies/hyper_evm.py +19 -0
  100. wayfinder_paths/policies/hyperlend.py +12 -0
  101. wayfinder_paths/policies/hyperliquid.py +30 -0
  102. wayfinder_paths/policies/moonwell.py +54 -0
  103. wayfinder_paths/policies/prjx.py +30 -0
  104. wayfinder_paths/policies/util.py +27 -0
  105. wayfinder_paths/run_strategy.py +411 -0
  106. wayfinder_paths/scripts/__init__.py +0 -0
  107. wayfinder_paths/scripts/create_strategy.py +181 -0
  108. wayfinder_paths/scripts/make_wallets.py +169 -0
  109. wayfinder_paths/scripts/run_strategy.py +124 -0
  110. wayfinder_paths/scripts/validate_manifests.py +213 -0
  111. wayfinder_paths/strategies/__init__.py +0 -0
  112. wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
  113. wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
  114. wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
  115. wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
  116. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
  117. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
  118. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
  119. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
  120. wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
  121. wayfinder_paths/strategies/config.py +85 -0
  122. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
  123. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
  124. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  125. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
  126. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
  127. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
  128. wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
  129. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  130. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
  131. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
  132. wayfinder_paths/templates/adapter/README.md +105 -0
  133. wayfinder_paths/templates/adapter/adapter.py +26 -0
  134. wayfinder_paths/templates/adapter/examples.json +8 -0
  135. wayfinder_paths/templates/adapter/manifest.yaml +6 -0
  136. wayfinder_paths/templates/adapter/test_adapter.py +49 -0
  137. wayfinder_paths/templates/strategy/README.md +153 -0
  138. wayfinder_paths/templates/strategy/examples.json +11 -0
  139. wayfinder_paths/templates/strategy/manifest.yaml +8 -0
  140. wayfinder_paths/templates/strategy/strategy.py +57 -0
  141. wayfinder_paths/templates/strategy/test_strategy.py +197 -0
  142. wayfinder_paths/tests/__init__.py +0 -0
  143. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  144. wayfinder_paths/tests/test_test_coverage.py +212 -0
  145. wayfinder_paths/tests/test_utils.py +64 -0
  146. wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
  147. wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
  148. wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
  149. 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"]