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,350 @@
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.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
+ "vault_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
43
+ }
44
+
45
+ s = StablecoinYieldStrategy(
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
+ 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
+ s.balance_adapter.get_all_balances = AsyncMock(
65
+ return_value=(True, {"balances": []})
66
+ )
67
+
68
+ if hasattr(s, "token_adapter") and s.token_adapter:
69
+ default_usdc = {
70
+ "id": "usd-coin-base",
71
+ "symbol": "USDC",
72
+ "name": "USD Coin",
73
+ "decimals": 6,
74
+ "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
75
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
76
+ }
77
+
78
+ default_pool_token = {
79
+ "id": "test-pool-base",
80
+ "symbol": "POOL",
81
+ "name": "Test Pool",
82
+ "decimals": 18,
83
+ "address": "0x1234567890123456789012345678901234567890",
84
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
85
+ }
86
+
87
+ def get_token_side_effect(address=None, token_id=None, **kwargs):
88
+ if token_id == "usd-coin-base" or token_id == "usd-coin":
89
+ return (True, default_usdc)
90
+ elif (
91
+ token_id == "test-pool-base"
92
+ or address == "0x1234567890123456789012345678901234567890"
93
+ ):
94
+ return (True, default_pool_token)
95
+ return (True, default_usdc)
96
+
97
+ s.token_adapter.get_token = AsyncMock(side_effect=get_token_side_effect)
98
+ s.token_adapter.get_gas_token = AsyncMock(
99
+ return_value=(
100
+ True,
101
+ {
102
+ "id": "ethereum-base",
103
+ "symbol": "ETH",
104
+ "name": "Ethereum",
105
+ "decimals": 18,
106
+ "address": "0x4200000000000000000000000000000000000006",
107
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
108
+ },
109
+ )
110
+ )
111
+
112
+ if hasattr(s, "balance_adapter") and s.balance_adapter:
113
+ s.balance_adapter.move_from_main_wallet_to_vault_wallet = AsyncMock(
114
+ return_value=(True, "Transfer successful (simulated)")
115
+ )
116
+ s.balance_adapter.move_from_vault_wallet_to_main_wallet = AsyncMock(
117
+ return_value=(True, "Transfer successful (simulated)")
118
+ )
119
+ if hasattr(s.balance_adapter, "wallet_provider"):
120
+ s.balance_adapter.wallet_provider.broadcast_transaction = AsyncMock(
121
+ return_value=(True, {"transaction_hash": "0xDEADBEEF"})
122
+ )
123
+
124
+ if hasattr(s, "ledger_adapter") and s.ledger_adapter:
125
+ s.ledger_adapter.get_vault_net_deposit = AsyncMock(
126
+ return_value=(True, {"net_deposit": 0})
127
+ )
128
+ s.ledger_adapter.get_vault_transactions = AsyncMock(
129
+ return_value=(True, {"transactions": []})
130
+ )
131
+
132
+ if hasattr(s, "pool_adapter") and s.pool_adapter:
133
+ s.pool_adapter.find_high_yield_pools = AsyncMock(
134
+ return_value=(True, {"pools": [], "total_found": 0})
135
+ )
136
+ s.pool_adapter.get_pools_by_ids = AsyncMock(
137
+ return_value=(
138
+ True,
139
+ {"pools": [{"id": "test-pool-base", "apy": 15.0, "symbol": "POOL"}]},
140
+ )
141
+ )
142
+ s.pool_adapter.get_llama_matches = AsyncMock(
143
+ return_value=(
144
+ True,
145
+ {
146
+ "matches": [
147
+ {
148
+ "llama_stablecoin": True,
149
+ "llama_il_risk": "no",
150
+ "llama_tvl_usd": 2000000,
151
+ "llama_apy_pct": 5.0,
152
+ "network": "base",
153
+ "address": "0x1234567890123456789012345678901234567890",
154
+ "token_id": "test-pool-base",
155
+ "pool_id": "test-pool-base",
156
+ "llama_combined_apy_pct": 15.0,
157
+ }
158
+ ]
159
+ },
160
+ )
161
+ )
162
+
163
+ if hasattr(s, "brap_adapter") and s.brap_adapter:
164
+
165
+ def get_swap_quote_side_effect(*args, **kwargs):
166
+ to_token_address = kwargs.get("to_token_address", "")
167
+ if to_token_address == "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913":
168
+ return (
169
+ True,
170
+ {
171
+ "quotes": {
172
+ "best_quote": {
173
+ "output_amount": "99900000",
174
+ }
175
+ }
176
+ },
177
+ )
178
+ return (
179
+ True,
180
+ {
181
+ "quotes": {
182
+ "best_quote": {
183
+ "output_amount": "105000000",
184
+ "input_amount": "50000000000000",
185
+ "toAmount": "105000000",
186
+ "estimatedGas": "1000000000",
187
+ "fromAmount": "100000000",
188
+ "fromToken": {"symbol": "USDC"},
189
+ "toToken": {"symbol": "POOL"},
190
+ }
191
+ }
192
+ },
193
+ )
194
+
195
+ s.brap_adapter.get_swap_quote = AsyncMock(
196
+ side_effect=get_swap_quote_side_effect
197
+ )
198
+
199
+ if (
200
+ hasattr(s, "brap_adapter")
201
+ and s.brap_adapter
202
+ and hasattr(s.brap_adapter, "swap_from_quote")
203
+ ):
204
+ s.brap_adapter.swap_from_quote = AsyncMock(return_value=None)
205
+ if hasattr(s, "brap_adapter") and hasattr(s.brap_adapter, "wallet_provider"):
206
+ s.brap_adapter.wallet_provider.broadcast_transaction = AsyncMock(
207
+ return_value=(True, {"transaction_hash": "0xBEEF"})
208
+ )
209
+
210
+ s.DEPOSIT_USDC = 0
211
+ s.usdc_token_info = {
212
+ "id": "usd-coin-base",
213
+ "symbol": "USDC",
214
+ "name": "USD Coin",
215
+ "decimals": 6,
216
+ "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
217
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
218
+ }
219
+ s.gas_token = {
220
+ "id": "ethereum-base",
221
+ "symbol": "ETH",
222
+ "name": "Ethereum",
223
+ "decimals": 18,
224
+ "address": "0x4200000000000000000000000000000000000006",
225
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
226
+ }
227
+ s.current_pool = {
228
+ "id": "usd-coin-base",
229
+ "symbol": "USDC",
230
+ "decimals": 6,
231
+ "chain": {"code": "base", "id": 8453, "name": "Base"},
232
+ }
233
+ s.current_pool_balance = 100000000
234
+ s.current_combined_apy_pct = 0.0
235
+ s.current_pool_data = None
236
+
237
+ if hasattr(s, "token_adapter") and s.token_adapter:
238
+ if not hasattr(s.token_adapter, "get_token_price"):
239
+ s.token_adapter.get_token_price = AsyncMock()
240
+
241
+ def get_token_price_side_effect(token_id):
242
+ if token_id == "ethereum-base":
243
+ return (True, {"current_price": 2000.0})
244
+ else:
245
+ return (True, {"current_price": 1.0})
246
+
247
+ s.token_adapter.get_token_price = AsyncMock(
248
+ side_effect=get_token_price_side_effect
249
+ )
250
+
251
+ async def mock_sweep_wallet(target_token):
252
+ pass
253
+
254
+ async def mock_refresh_current_pool_balance():
255
+ pass
256
+
257
+ async def mock_rebalance_gas(target_pool):
258
+ return (True, "Gas rebalanced")
259
+
260
+ async def mock_has_idle_assets(balances, target):
261
+ return True
262
+
263
+ s._sweep_wallet = mock_sweep_wallet
264
+ s._refresh_current_pool_balance = mock_refresh_current_pool_balance
265
+ s._rebalance_gas = mock_rebalance_gas
266
+ s._has_idle_assets = mock_has_idle_assets
267
+
268
+ return s
269
+
270
+
271
+ @pytest.mark.asyncio
272
+ @pytest.mark.smoke
273
+ async def test_smoke(strategy):
274
+ """REQUIRED: Basic smoke test - verifies strategy lifecycle."""
275
+ examples = load_strategy_examples(Path(__file__))
276
+ smoke_data = examples["smoke"]
277
+
278
+ st = await strategy.status()
279
+ assert isinstance(st, dict)
280
+ assert "portfolio_value" in st or "net_deposit" in st or "strategy_status" in st
281
+
282
+ deposit_params = smoke_data.get("deposit", {})
283
+ ok, msg = await strategy.deposit(**deposit_params)
284
+ assert isinstance(ok, bool)
285
+ assert isinstance(msg, str)
286
+
287
+ ok, msg = await strategy.update(**smoke_data.get("update", {}))
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
+ ok, msg = await strategy.update()
312
+ assert ok, f"Canonical example '{example_name}' update failed: {msg}"
313
+
314
+ if "status" in example_data:
315
+ st = await strategy.status()
316
+ assert isinstance(st, dict), (
317
+ f"Canonical example '{example_name}' status failed"
318
+ )
319
+
320
+
321
+ @pytest.mark.asyncio
322
+ async def test_error_cases(strategy):
323
+ """OPTIONAL: Test error scenarios from examples.json."""
324
+ examples = load_strategy_examples(Path(__file__))
325
+
326
+ for example_name, example_data in examples.items():
327
+ if isinstance(example_data, dict) and "expect" in example_data:
328
+ expect = example_data.get("expect", {})
329
+
330
+ if "deposit" in example_data:
331
+ deposit_params = example_data.get("deposit", {})
332
+ ok, _ = await strategy.deposit(**deposit_params)
333
+
334
+ if expect.get("success") is False:
335
+ assert ok is False, (
336
+ f"Expected {example_name} deposit to fail but it succeeded"
337
+ )
338
+ elif expect.get("success") is True:
339
+ assert ok is True, (
340
+ f"Expected {example_name} deposit to succeed but it failed"
341
+ )
342
+
343
+ if "update" in example_data:
344
+ ok, _ = await strategy.update()
345
+ if "success" in expect:
346
+ expected_success = expect.get("success")
347
+ assert ok == expected_success, (
348
+ f"Expected {example_name} update to "
349
+ f"{'succeed' if expected_success else 'fail'} but got opposite"
350
+ )
@@ -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/vaults/templates/adapter wayfinder_paths/vaults/adapters/my_adapter
10
+ ```
11
+ 2. Rename `MyAdapter` in `adapter.py` and update `manifest.yaml` so the `entrypoint` matches (`vaults.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: "vaults.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.vaults.adapters.my_adapter.adapter import MyAdapter
84
+
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_get_pools():
88
+ with patch(
89
+ "wayfinder_paths.vaults.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: "vaults.adapters.my_adapter.adapter.MyAdapter"
3
+ capabilities:
4
+ - "example.op"
5
+ dependencies:
6
+ - "MyClient"
@@ -0,0 +1,49 @@
1
+ """Test template for adapters.
2
+
3
+ Quick setup:
4
+ 1. Replace MyAdapter with your actual adapter class name
5
+ 2. Implement test_basic_functionality with your adapter's core methods
6
+ 3. Add client mocking if your adapter uses external clients
7
+ 4. Run: pytest vaults/adapters/your_adapter/ -v
8
+
9
+ Note: examples.json is optional for adapters (not required).
10
+ """
11
+
12
+ import pytest
13
+
14
+ # TODO: Replace MyAdapter with your actual adapter class name
15
+ from .adapter import MyAdapter
16
+
17
+ # For mocking clients, uncomment when needed:
18
+ # from unittest.mock import AsyncMock, patch
19
+
20
+
21
+ class TestMyAdapter:
22
+ """Test cases for MyAdapter"""
23
+
24
+ @pytest.fixture
25
+ def adapter(self):
26
+ """Create adapter instance for testing."""
27
+ return MyAdapter(config={})
28
+
29
+ @pytest.mark.asyncio
30
+ async def test_health_check(self, adapter):
31
+ """Test adapter health check"""
32
+ health = await adapter.health_check()
33
+ assert isinstance(health, dict)
34
+ assert health.get("status") in {"healthy", "unhealthy", "error"}
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_connect(self, adapter):
38
+ """Test adapter connection"""
39
+ ok = await adapter.connect()
40
+ assert isinstance(ok, bool)
41
+
42
+ def test_capabilities(self, adapter):
43
+ """Test adapter capabilities match manifest"""
44
+ assert hasattr(adapter, "adapter_type")
45
+
46
+ @pytest.mark.asyncio
47
+ async def test_basic_functionality(self, adapter):
48
+ """REQUIRED: Test your adapter's core functionality."""
49
+ assert adapter is not None