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.
- wayfinder_paths/CONFIG_GUIDE.md +394 -0
- wayfinder_paths/__init__.py +21 -0
- wayfinder_paths/config.example.json +20 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +13 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +90 -0
- wayfinder_paths/core/clients/ClientManager.py +231 -0
- wayfinder_paths/core/clients/HyperlendClient.py +151 -0
- wayfinder_paths/core/clients/LedgerClient.py +222 -0
- wayfinder_paths/core/clients/PoolClient.py +96 -0
- wayfinder_paths/core/clients/SimulationClient.py +180 -0
- wayfinder_paths/core/clients/TokenClient.py +73 -0
- wayfinder_paths/core/clients/TransactionClient.py +47 -0
- wayfinder_paths/core/clients/WalletClient.py +90 -0
- wayfinder_paths/core/clients/WayfinderClient.py +258 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +295 -0
- wayfinder_paths/core/clients/sdk_example.py +115 -0
- wayfinder_paths/core/config.py +369 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +25 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/VaultJob.py +182 -0
- wayfinder_paths/core/engine/__init__.py +5 -0
- wayfinder_paths/core/engine/manifest.py +97 -0
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +177 -0
- wayfinder_paths/core/services/local_evm_txn.py +429 -0
- wayfinder_paths/core/services/local_token_txn.py +231 -0
- wayfinder_paths/core/services/web3_service.py +45 -0
- wayfinder_paths/core/settings.py +61 -0
- wayfinder_paths/core/strategies/Strategy.py +183 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +165 -0
- wayfinder_paths/core/utils/wallets.py +77 -0
- wayfinder_paths/core/wallets/README.md +91 -0
- wayfinder_paths/core/wallets/WalletManager.py +56 -0
- wayfinder_paths/core/wallets/__init__.py +7 -0
- wayfinder_paths/run_strategy.py +409 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +160 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/tests/__init__.py +0 -0
- wayfinder_paths/tests/test_smoke_manifest.py +48 -0
- wayfinder_paths/tests/test_test_coverage.py +212 -0
- wayfinder_paths/tests/test_utils.py +64 -0
- wayfinder_paths/vaults/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
- wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
- wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
- wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
- wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
- wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
- wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
- wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
- wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
- wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
- wayfinder_paths/vaults/strategies/__init__.py +0 -0
- wayfinder_paths/vaults/strategies/config.py +85 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
- wayfinder_paths/vaults/templates/adapter/README.md +105 -0
- wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
- wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
- wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/vaults/templates/strategy/README.md +152 -0
- wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
- wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
- wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
- wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
- wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
- 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,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
|