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,520 @@
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.stablecoin_yield_strategy.strategy import ( # noqa: E402
33
+ StablecoinYieldStrategy,
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 = StablecoinYieldStrategy(
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
+ usdc_balance_mock = AsyncMock(return_value=(True, 60000000))
54
+ gas_balance_mock = AsyncMock(return_value=(True, 2000000000000000))
55
+
56
+ def get_balance_side_effect(token_id, wallet_address, **kwargs):
57
+ if token_id == "usd-coin-base" or token_id == "usd-coin":
58
+ return usdc_balance_mock.return_value
59
+ elif token_id == "ethereum-base" or token_id == "ethereum":
60
+ return gas_balance_mock.return_value
61
+ return (True, 1000000000)
62
+
63
+ s.balance_adapter.get_balance = AsyncMock(side_effect=get_balance_side_effect)
64
+
65
+ if hasattr(s, "token_adapter") and s.token_adapter:
66
+ default_usdc = {
67
+ "id": "usd-coin-base",
68
+ "symbol": "USDC",
69
+ "name": "USD Coin",
70
+ "decimals": 6,
71
+ "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
72
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
73
+ }
74
+
75
+ default_pool_token = {
76
+ "id": "test-pool-base",
77
+ "symbol": "POOL",
78
+ "name": "Test Pool",
79
+ "decimals": 18,
80
+ "address": "0x1234567890123456789012345678901234567890",
81
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
82
+ }
83
+
84
+ def get_token_side_effect(address=None, token_id=None, **kwargs):
85
+ if token_id == "usd-coin-base" or token_id == "usd-coin":
86
+ return (True, default_usdc)
87
+ elif (
88
+ token_id == "test-pool-base"
89
+ or address == "0x1234567890123456789012345678901234567890"
90
+ ):
91
+ return (True, default_pool_token)
92
+ return (True, default_usdc)
93
+
94
+ s.token_adapter.get_token = AsyncMock(side_effect=get_token_side_effect)
95
+ s.token_adapter.get_gas_token = AsyncMock(
96
+ return_value=(
97
+ True,
98
+ {
99
+ "id": "ethereum-base",
100
+ "symbol": "ETH",
101
+ "name": "Ethereum",
102
+ "decimals": 18,
103
+ "address": "0x4200000000000000000000000000000000000006",
104
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
105
+ },
106
+ )
107
+ )
108
+
109
+ if hasattr(s, "balance_adapter") and s.balance_adapter:
110
+ s.balance_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
111
+ return_value=(True, "Transfer successful (simulated)")
112
+ )
113
+ s.balance_adapter.move_from_strategy_wallet_to_main_wallet = AsyncMock(
114
+ return_value=(True, "Transfer successful (simulated)")
115
+ )
116
+ if hasattr(s.balance_adapter, "wallet_provider"):
117
+ s.balance_adapter.wallet_provider.broadcast_transaction = AsyncMock(
118
+ return_value=(True, {"transaction_hash": "0xDEADBEEF"})
119
+ )
120
+
121
+ if hasattr(s, "ledger_adapter") and s.ledger_adapter:
122
+ s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
123
+ return_value=(True, {"net_deposit": 0})
124
+ )
125
+ s.ledger_adapter.get_strategy_transactions = AsyncMock(
126
+ return_value=(True, {"transactions": []})
127
+ )
128
+
129
+ if hasattr(s, "pool_adapter") and s.pool_adapter:
130
+ s.pool_adapter.find_high_yield_pools = AsyncMock(
131
+ return_value=(True, {"pools": [], "total_found": 0})
132
+ )
133
+ s.pool_adapter.get_pools_by_ids = AsyncMock(
134
+ return_value=(
135
+ True,
136
+ {"pools": [{"id": "test-pool-base", "apy": 15.0, "symbol": "POOL"}]},
137
+ )
138
+ )
139
+ s.pool_adapter.get_llama_matches = AsyncMock(
140
+ return_value=(
141
+ True,
142
+ {
143
+ "matches": [
144
+ {
145
+ "llama_stablecoin": True,
146
+ "llama_il_risk": "no",
147
+ "llama_tvl_usd": 2000000,
148
+ "llama_apy_pct": 5.0,
149
+ "network": "base",
150
+ "address": "0x1234567890123456789012345678901234567890",
151
+ "token_id": "test-pool-base",
152
+ "pool_id": "test-pool-base",
153
+ "llama_combined_apy_pct": 15.0,
154
+ }
155
+ ]
156
+ },
157
+ )
158
+ )
159
+
160
+ if hasattr(s, "brap_adapter") and s.brap_adapter:
161
+
162
+ def get_swap_quote_side_effect(*args, **kwargs):
163
+ to_token_address = kwargs.get("to_token_address", "")
164
+ if to_token_address == "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913":
165
+ return (
166
+ True,
167
+ {
168
+ "quotes": {
169
+ "best_quote": {
170
+ "output_amount": "99900000",
171
+ }
172
+ }
173
+ },
174
+ )
175
+ return (
176
+ True,
177
+ {
178
+ "quotes": {
179
+ "best_quote": {
180
+ "output_amount": "105000000",
181
+ "input_amount": "50000000000000",
182
+ "toAmount": "105000000",
183
+ "estimatedGas": "1000000000",
184
+ "fromAmount": "100000000",
185
+ "fromToken": {"symbol": "USDC"},
186
+ "toToken": {"symbol": "POOL"},
187
+ }
188
+ }
189
+ },
190
+ )
191
+
192
+ s.brap_adapter.get_swap_quote = AsyncMock(
193
+ side_effect=get_swap_quote_side_effect
194
+ )
195
+
196
+ if (
197
+ hasattr(s, "brap_adapter")
198
+ and s.brap_adapter
199
+ and hasattr(s.brap_adapter, "swap_from_quote")
200
+ ):
201
+ s.brap_adapter.swap_from_quote = AsyncMock(return_value=None)
202
+ if hasattr(s, "brap_adapter") and hasattr(s.brap_adapter, "wallet_provider"):
203
+ s.brap_adapter.wallet_provider.broadcast_transaction = AsyncMock(
204
+ return_value=(True, {"transaction_hash": "0xBEEF"})
205
+ )
206
+
207
+ s.DEPOSIT_USDC = 0
208
+ s.usdc_token_info = {
209
+ "id": "usd-coin-base",
210
+ "symbol": "USDC",
211
+ "name": "USD Coin",
212
+ "decimals": 6,
213
+ "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
214
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
215
+ }
216
+ s.gas_token = {
217
+ "id": "ethereum-base",
218
+ "symbol": "ETH",
219
+ "name": "Ethereum",
220
+ "decimals": 18,
221
+ "address": "0x4200000000000000000000000000000000000006",
222
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
223
+ }
224
+ s.current_pool = {
225
+ "id": "usd-coin-base",
226
+ "symbol": "USDC",
227
+ "decimals": 6,
228
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
229
+ }
230
+ s.current_pool_balance = 100000000
231
+ s.current_combined_apy_pct = 0.0
232
+ s.current_pool_data = None
233
+
234
+ if hasattr(s, "token_adapter") and s.token_adapter:
235
+ if not hasattr(s.token_adapter, "get_token_price"):
236
+ s.token_adapter.get_token_price = AsyncMock()
237
+
238
+ def get_token_price_side_effect(token_id):
239
+ if token_id == "ethereum-base":
240
+ return (True, {"current_price": 2000.0})
241
+ else:
242
+ return (True, {"current_price": 1.0})
243
+
244
+ s.token_adapter.get_token_price = AsyncMock(
245
+ side_effect=get_token_price_side_effect
246
+ )
247
+
248
+ async def mock_sweep_wallet(target_token):
249
+ pass
250
+
251
+ async def mock_refresh_current_pool_balance():
252
+ pass
253
+
254
+ async def mock_rebalance_gas(target_pool):
255
+ return (True, "Gas rebalanced")
256
+
257
+ async def mock_has_idle_assets(balances, target):
258
+ return True
259
+
260
+ s._sweep_wallet = mock_sweep_wallet
261
+ s._refresh_current_pool_balance = mock_refresh_current_pool_balance
262
+ s._rebalance_gas = mock_rebalance_gas
263
+ s._has_idle_assets = mock_has_idle_assets
264
+
265
+ return s
266
+
267
+
268
+ @pytest.mark.asyncio
269
+ @pytest.mark.smoke
270
+ async def test_smoke(strategy):
271
+ """REQUIRED: Basic smoke test - verifies strategy lifecycle."""
272
+ examples = load_strategy_examples(Path(__file__))
273
+ smoke_data = examples["smoke"]
274
+
275
+ st = await strategy.status()
276
+ assert isinstance(st, dict)
277
+ assert "portfolio_value" in st or "net_deposit" in st or "strategy_status" in st
278
+
279
+ deposit_params = smoke_data.get("deposit", {})
280
+ ok, msg = await strategy.deposit(**deposit_params)
281
+ assert isinstance(ok, bool)
282
+ assert isinstance(msg, str)
283
+
284
+ ok, msg = await strategy.update(**smoke_data.get("update", {}))
285
+ assert isinstance(ok, bool)
286
+
287
+ ok, msg = await strategy.withdraw(**smoke_data.get("withdraw", {}))
288
+ assert isinstance(ok, bool)
289
+
290
+
291
+ @pytest.mark.asyncio
292
+ async def test_canonical_usage(strategy):
293
+ """REQUIRED: Test canonical usage examples from examples.json (minimum).
294
+
295
+ Canonical usage = all positive usage examples (excluding error cases).
296
+ This is the MINIMUM requirement - feel free to add more test cases here.
297
+ """
298
+ examples = load_strategy_examples(Path(__file__))
299
+ canonical = get_canonical_examples(examples)
300
+
301
+ for example_name, example_data in canonical.items():
302
+ if "deposit" in example_data:
303
+ deposit_params = example_data.get("deposit", {})
304
+ ok, _ = await strategy.deposit(**deposit_params)
305
+ assert ok, f"Canonical example '{example_name}' deposit failed"
306
+
307
+ if "update" in example_data:
308
+ ok, msg = await strategy.update()
309
+ assert ok, f"Canonical example '{example_name}' update failed: {msg}"
310
+
311
+ if "status" in example_data:
312
+ st = await strategy.status()
313
+ assert isinstance(st, dict), (
314
+ f"Canonical example '{example_name}' status failed"
315
+ )
316
+
317
+
318
+ @pytest.mark.asyncio
319
+ async def test_error_cases(strategy):
320
+ """OPTIONAL: Test error scenarios from examples.json."""
321
+ examples = load_strategy_examples(Path(__file__))
322
+
323
+ for example_name, example_data in examples.items():
324
+ if isinstance(example_data, dict) and "expect" in example_data:
325
+ expect = example_data.get("expect", {})
326
+
327
+ if "deposit" in example_data:
328
+ deposit_params = example_data.get("deposit", {})
329
+ ok, _ = await strategy.deposit(**deposit_params)
330
+
331
+ if expect.get("success") is False:
332
+ assert ok is False, (
333
+ f"Expected {example_name} deposit to fail but it succeeded"
334
+ )
335
+ elif expect.get("success") is True:
336
+ assert ok is True, (
337
+ f"Expected {example_name} deposit to succeed but it failed"
338
+ )
339
+
340
+ if "update" in example_data:
341
+ ok, _ = await strategy.update()
342
+ if "success" in expect:
343
+ expected_success = expect.get("success")
344
+ assert ok == expected_success, (
345
+ f"Expected {example_name} update to "
346
+ f"{'succeed' if expected_success else 'fail'} but got opposite"
347
+ )
348
+
349
+
350
+ @pytest.mark.asyncio
351
+ async def test_token_tracking_initialization(strategy):
352
+ """Test that tracked_token_ids and tracked_balances are initialized."""
353
+ assert hasattr(strategy, "tracked_token_ids")
354
+ assert hasattr(strategy, "tracked_balances")
355
+ assert isinstance(strategy.tracked_token_ids, set)
356
+ assert isinstance(strategy.tracked_balances, dict)
357
+
358
+
359
+ @pytest.mark.asyncio
360
+ async def test_track_token(strategy):
361
+ """Test that _track_token adds tokens to tracked state."""
362
+ test_token_id = "test-token-base"
363
+ test_balance = 1000000
364
+
365
+ strategy._track_token(test_token_id, test_balance)
366
+
367
+ assert test_token_id in strategy.tracked_token_ids
368
+ assert strategy.tracked_balances.get(test_token_id) == test_balance
369
+
370
+
371
+ @pytest.mark.asyncio
372
+ async def test_update_balance(strategy):
373
+ """Test that _update_balance updates tracked balances."""
374
+ test_token_id = "test-token-base"
375
+ initial_balance = 1000000
376
+ updated_balance = 2000000
377
+
378
+ strategy._track_token(test_token_id, initial_balance)
379
+ assert strategy.tracked_balances.get(test_token_id) == initial_balance
380
+
381
+ strategy._update_balance(test_token_id, updated_balance)
382
+ assert strategy.tracked_balances.get(test_token_id) == updated_balance
383
+
384
+
385
+ @pytest.mark.asyncio
386
+ async def test_get_non_zero_tracked_tokens(strategy):
387
+ """Test that _get_non_zero_tracked_tokens returns only non-zero balances."""
388
+ strategy._track_token("token-1", 1000000)
389
+ strategy._track_token("token-2", 0)
390
+ strategy._track_token("token-3", 5000000)
391
+
392
+ non_zero = strategy._get_non_zero_tracked_tokens()
393
+
394
+ assert len(non_zero) == 2
395
+ token_ids = [token_id for token_id, _ in non_zero]
396
+ assert "token-1" in token_ids
397
+ assert "token-3" in token_ids
398
+ assert "token-2" not in token_ids
399
+
400
+
401
+ @pytest.mark.asyncio
402
+ async def test_refresh_tracked_balances(strategy):
403
+ """Test that _refresh_tracked_balances updates all tracked token balances."""
404
+ # Track some tokens
405
+ strategy._track_token("usd-coin-base")
406
+ strategy._track_token("ethereum-base")
407
+
408
+ # Refresh balances
409
+ await strategy._refresh_tracked_balances()
410
+
411
+ # Verify balances were fetched
412
+ assert "usd-coin-base" in strategy.tracked_balances
413
+ assert "ethereum-base" in strategy.tracked_balances
414
+
415
+
416
+ @pytest.mark.asyncio
417
+ async def test_deposit_tracks_usdc(strategy):
418
+ """Test that deposit operation tracks USDC token."""
419
+ # Clear tracked state
420
+ strategy.tracked_token_ids.clear()
421
+ strategy.tracked_balances.clear()
422
+
423
+ # Perform deposit
424
+ ok, _ = await strategy.deposit(main_token_amount=100.0)
425
+
426
+ # Verify USDC is tracked
427
+ assert ok
428
+ usdc_token_id = strategy.usdc_token_info.get("token_id")
429
+ assert usdc_token_id in strategy.tracked_token_ids
430
+
431
+
432
+ @pytest.mark.asyncio
433
+ async def test_sweep_wallet_uses_tracked_tokens(strategy):
434
+ """Test that _sweep_wallet only swaps tracked tokens."""
435
+ # Setup: track some tokens with balances
436
+ strategy._track_token("token-1", 1000000)
437
+ strategy._track_token("token-2", 2000000)
438
+
439
+ # Mock balance adapter to return fresh balances
440
+ strategy.balance_adapter.get_balance = AsyncMock(
441
+ side_effect=lambda token_id, **kwargs: (
442
+ True,
443
+ strategy.tracked_balances.get(token_id, 0),
444
+ )
445
+ )
446
+
447
+ # Mock brap adapter swap
448
+ strategy.brap_adapter.swap_from_token_ids = AsyncMock(
449
+ return_value=(True, "Swap successful")
450
+ )
451
+
452
+ target_token = {
453
+ "token_id": "usd-coin-base",
454
+ "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
455
+ "chain": {"code": "base", "name": "Base"},
456
+ }
457
+
458
+ # Call sweep
459
+ await strategy._sweep_wallet(target_token)
460
+
461
+ # Verify that swap was called for tracked tokens
462
+ assert strategy.brap_adapter.swap_from_token_ids.called
463
+
464
+
465
+ @pytest.mark.asyncio
466
+ async def test_get_non_gas_balances_uses_tracked_state(strategy):
467
+ """Test that _get_non_gas_balances only checks tracked tokens."""
468
+ # Setup tracked tokens
469
+ usdc_token_id = "usd-coin-base"
470
+ pool_token_id = "test-pool-base"
471
+
472
+ strategy._track_token(usdc_token_id, 100000000)
473
+ strategy._track_token(pool_token_id, 50000000000000000000)
474
+
475
+ # Mock refresh
476
+ strategy.balance_adapter.get_balance = AsyncMock(
477
+ side_effect=lambda token_id, **kwargs: (
478
+ True,
479
+ strategy.tracked_balances.get(token_id, 0),
480
+ )
481
+ )
482
+
483
+ # Get non-gas balances
484
+ balances = await strategy._get_non_gas_balances()
485
+
486
+ # Verify only tracked tokens are returned (excluding gas)
487
+ token_ids = [b["token_id"] for b in balances]
488
+ assert usdc_token_id in token_ids or pool_token_id in token_ids
489
+ assert len(balances) <= len(strategy.tracked_token_ids)
490
+
491
+
492
+ @pytest.mark.asyncio
493
+ async def test_partial_liquidate_uses_tracked_tokens(strategy):
494
+ """Test that partial_liquidate only liquidates tracked tokens."""
495
+ # Setup tracked tokens with balances
496
+ strategy._track_token("usd-coin-base", 50000000) # 50 USDC
497
+ strategy._track_token("test-pool-base", 100000000000000000000) # 100 POOL tokens
498
+
499
+ # Mock balance and token adapters
500
+ strategy.balance_adapter.get_balance = AsyncMock(
501
+ side_effect=lambda token_id, **kwargs: (
502
+ True,
503
+ strategy.tracked_balances.get(token_id, 0),
504
+ )
505
+ )
506
+
507
+ strategy.token_adapter.get_token_price = AsyncMock(
508
+ return_value=(True, {"current_price": 1.0})
509
+ )
510
+
511
+ strategy.brap_adapter.swap_from_token_ids = AsyncMock(
512
+ return_value=(True, "Swap successful")
513
+ )
514
+
515
+ # Call partial liquidate
516
+ ok, msg = await strategy.partial_liquidate(usd_value=75.0)
517
+
518
+ # Verify success
519
+ assert ok
520
+ assert "liquidation completed" in msg.lower()
@@ -0,0 +1,105 @@
1
+ # Adapter Template
2
+
3
+ Adapters expose protocol-specific capabilities to strategies. They should be thin, async wrappers around one or more clients from `wayfinder_paths.core.clients`.
4
+
5
+ ## Quick start
6
+
7
+ 1. Copy the template:
8
+ ```
9
+ cp -r wayfinder_paths/templates/adapter wayfinder_paths/adapters/my_adapter
10
+ ```
11
+ 2. Rename `MyAdapter` in `adapter.py` and update `manifest.yaml` so the `entrypoint` matches (`adapters.my_adapter.adapter.MyAdapter`).
12
+ 3. Declare the capabilities your adapter will provide and list any client dependencies (e.g., `PoolClient`, `LedgerClient`).
13
+ 4. Implement the public methods that fulfill those capabilities.
14
+
15
+ ## Layout
16
+
17
+ ```
18
+ my_adapter/
19
+ ├── adapter.py # Adapter implementation
20
+ ├── manifest.yaml # Entrypoint + capabilities + dependency list
21
+ ├── examples.json # Example payloads (optional but encouraged)
22
+ ├── test_adapter.py # Pytest smoke tests
23
+ └── README.md # Adapter-specific notes
24
+ ```
25
+
26
+ ## Skeleton adapter
27
+
28
+ ```python
29
+ from typing import Any
30
+
31
+ from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
32
+ from wayfinder_paths.core.clients.PoolClient import PoolClient
33
+
34
+
35
+ class MyAdapter(BaseAdapter):
36
+ adapter_type = "MY_ADAPTER"
37
+
38
+ def __init__(self, config: dict[str, Any] | None = None):
39
+ super().__init__("my_adapter", config)
40
+ self.pool_client = PoolClient()
41
+
42
+ async def connect(self) -> bool:
43
+ """Optional: prime caches / test connectivity."""
44
+ return True
45
+
46
+ async def get_pools(self, pool_ids: list[str]) -> tuple[bool, Any]:
47
+ """Example capability that proxies PoolClient."""
48
+ try:
49
+ data = await self.pool_client.get_pools_by_ids(
50
+ pool_ids=",".join(pool_ids), merge_external=True
51
+ )
52
+ return (True, data)
53
+ except Exception as exc: # noqa: BLE001
54
+ self.logger.error(f"Failed to fetch pools: {exc}")
55
+ return (False, str(exc))
56
+ ```
57
+
58
+ Your adapter should return `(success, payload)` tuples for every operation, just like the built-in adapters do.
59
+
60
+ ## Manifest
61
+
62
+ Every adapter needs a manifest describing its import path, declared capabilities, and runtime dependencies.
63
+
64
+ ```yaml
65
+ schema_version: "0.1"
66
+ entrypoint: "adapters.my_adapter.adapter.MyAdapter"
67
+ capabilities:
68
+ - "pool.read"
69
+ dependencies:
70
+ - "PoolClient"
71
+ ```
72
+
73
+ The `dependencies` list is informational today but helps reviewers understand which core clients you rely on.
74
+
75
+ ## Testing
76
+
77
+ `test_adapter.py` should cover the public methods you expose. Patch out remote clients with `unittest.mock.AsyncMock` so tests run offline.
78
+
79
+ ```python
80
+ import pytest
81
+ from unittest.mock import AsyncMock, patch
82
+
83
+ from wayfinder_paths.adapters.my_adapter.adapter import MyAdapter
84
+
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_get_pools():
88
+ with patch(
89
+ "wayfinder_paths.adapters.my_adapter.adapter.PoolClient",
90
+ return_value=AsyncMock(
91
+ get_pools_by_ids=AsyncMock(return_value={"pools": []})
92
+ ),
93
+ ):
94
+ adapter = MyAdapter(config={})
95
+ success, data = await adapter.get_pools(["pool-1"])
96
+ assert success is True
97
+ assert "pools" in data
98
+ ```
99
+
100
+ ## Best practices
101
+
102
+ - Capabilities listed in `manifest.yaml` must correspond to methods you implement.
103
+ - Keep adapters stateless and idempotent—strategies may reuse instances across operations.
104
+ - Use `self.logger` for contextual logging (BaseAdapter has already bound the adapter name).
105
+ - Raise `NotImplementedError` for manifest capabilities you intentionally do not support yet.
@@ -0,0 +1,26 @@
1
+ from typing import Any
2
+
3
+ from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
4
+
5
+
6
+ class MyAdapter(BaseAdapter):
7
+ """
8
+ Template adapter for a protocol/exchange integration.
9
+ Copy this folder, rename it (e.g., my_adapter), update manifest entrypoint,
10
+ and implement the capabilities your manifest declares.
11
+ """
12
+
13
+ adapter_type: str = "MY_ADAPTER"
14
+
15
+ def __init__(self, config: dict[str, Any] | None = None):
16
+ super().__init__("my_adapter", config)
17
+
18
+ async def connect(self) -> bool:
19
+ """Establish connectivity to remote service(s) if needed."""
20
+ return True
21
+
22
+ async def example_operation(self, **kwargs) -> tuple[bool, str]:
23
+ """
24
+ Example operation. Replace with your adapter's real API.
25
+ """
26
+ return (True, "example.op executed")
@@ -0,0 +1,8 @@
1
+ {
2
+ "connect": {},
3
+ "example_operation": {
4
+ "args": {"foo": "bar"}
5
+ }
6
+ }
7
+
8
+
@@ -0,0 +1,6 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "adapters.my_adapter.adapter.MyAdapter"
3
+ capabilities:
4
+ - "example.op"
5
+ dependencies:
6
+ - "MyClient"