wayfinder-paths 0.1.9__py3-none-any.whl → 0.1.10__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 +1 -1
- wayfinder_paths/adapters/balance_adapter/README.md +1 -2
- wayfinder_paths/adapters/balance_adapter/adapter.py +4 -4
- wayfinder_paths/adapters/brap_adapter/adapter.py +139 -23
- wayfinder_paths/adapters/moonwell_adapter/README.md +174 -0
- wayfinder_paths/adapters/moonwell_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +1226 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +635 -0
- wayfinder_paths/core/clients/AuthClient.py +3 -0
- wayfinder_paths/core/clients/WayfinderClient.py +2 -2
- wayfinder_paths/core/constants/__init__.py +0 -2
- wayfinder_paths/core/constants/base.py +6 -2
- wayfinder_paths/core/constants/moonwell_abi.py +411 -0
- wayfinder_paths/core/engine/StrategyJob.py +3 -0
- wayfinder_paths/core/services/base.py +55 -0
- wayfinder_paths/core/services/local_evm_txn.py +288 -208
- wayfinder_paths/core/services/local_token_txn.py +46 -26
- wayfinder_paths/core/strategies/descriptors.py +1 -1
- wayfinder_paths/run_strategy.py +34 -74
- wayfinder_paths/scripts/create_strategy.py +2 -27
- wayfinder_paths/scripts/run_strategy.py +37 -7
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +1 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -15
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +108 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/examples.json +11 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2975 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +886 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1 -1
- wayfinder_paths/templates/adapter/README.md +5 -21
- wayfinder_paths/templates/adapter/adapter.py +1 -2
- wayfinder_paths/templates/adapter/test_adapter.py +1 -1
- wayfinder_paths/templates/strategy/README.md +4 -21
- wayfinder_paths/tests/test_smoke_manifest.py +17 -2
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/METADATA +60 -187
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/RECORD +39 -44
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/token_adapter/manifest.yaml +0 -6
- wayfinder_paths/core/engine/manifest.py +0 -97
- wayfinder_paths/scripts/validate_manifests.py +0 -213
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +0 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +0 -17
- wayfinder_paths/templates/adapter/manifest.yaml +0 -6
- wayfinder_paths/templates/strategy/manifest.yaml +0 -8
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/WHEEL +0 -0
wayfinder_paths/CONFIG_GUIDE.md
CHANGED
|
@@ -138,7 +138,7 @@ just create-strategy "My Strategy Name"
|
|
|
138
138
|
# This automatically:
|
|
139
139
|
# - Creates the strategy directory
|
|
140
140
|
# - Generates a wallet with label matching the directory name
|
|
141
|
-
# - Updates the
|
|
141
|
+
# - Updates the strategy files with the correct names
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
### Wallet Structure
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
Adapter that exposes wallet, token, and pool balances backed by `WalletClient`/`TokenClient` and now orchestrates transfers between the configured main/strategy wallets (with ledger bookkeeping).
|
|
4
4
|
|
|
5
5
|
- Entrypoint: `adapters.balance_adapter.adapter.BalanceAdapter`
|
|
6
|
-
- Manifest: `manifest.yaml`
|
|
7
6
|
- Tests: `test_adapter.py`
|
|
8
7
|
|
|
9
8
|
## Capabilities
|
|
10
9
|
|
|
11
|
-
The adapter
|
|
10
|
+
The adapter provides both wallet read and wallet transfer capabilities. Transfers are executed by leveraging the shared `DefaultWeb3Service.token_transactions` helper, but ledger recording + wallet selection now live inside the adapter.
|
|
12
11
|
|
|
13
12
|
## Construction
|
|
14
13
|
|
|
@@ -170,7 +170,7 @@ class BalanceAdapter(BaseAdapter):
|
|
|
170
170
|
token_amount=str(amount),
|
|
171
171
|
usd_value=usd_value,
|
|
172
172
|
data={
|
|
173
|
-
"token_id": token_info.get("
|
|
173
|
+
"token_id": token_info.get("token_id"),
|
|
174
174
|
"amount": str(amount),
|
|
175
175
|
"usd_value": usd_value,
|
|
176
176
|
},
|
|
@@ -180,7 +180,7 @@ class BalanceAdapter(BaseAdapter):
|
|
|
180
180
|
self.logger.warning(
|
|
181
181
|
"Ledger entry failed",
|
|
182
182
|
wallet=wallet_address,
|
|
183
|
-
token_id=token_info.get("
|
|
183
|
+
token_id=token_info.get("token_id"),
|
|
184
184
|
amount=amount,
|
|
185
185
|
error=response,
|
|
186
186
|
)
|
|
@@ -188,13 +188,13 @@ class BalanceAdapter(BaseAdapter):
|
|
|
188
188
|
self.logger.warning(
|
|
189
189
|
f"Ledger entry raised: {exc}",
|
|
190
190
|
wallet=wallet_address,
|
|
191
|
-
token_id=token_info.get("
|
|
191
|
+
token_id=token_info.get("token_id"),
|
|
192
192
|
)
|
|
193
193
|
|
|
194
194
|
async def _token_amount_usd(
|
|
195
195
|
self, token_info: dict[str, Any], amount: float
|
|
196
196
|
) -> float:
|
|
197
|
-
token_id = token_info.get("
|
|
197
|
+
token_id = token_info.get("token_id")
|
|
198
198
|
if not token_id:
|
|
199
199
|
return 0.0
|
|
200
200
|
success, price_data = await self.token_adapter.get_token_price(token_id)
|
|
@@ -11,7 +11,7 @@ from wayfinder_paths.core.adapters.models import SWAP
|
|
|
11
11
|
from wayfinder_paths.core.clients.BRAPClient import BRAPClient, BRAPQuote
|
|
12
12
|
from wayfinder_paths.core.clients.LedgerClient import TransactionRecord
|
|
13
13
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
14
|
-
from wayfinder_paths.core.constants import DEFAULT_SLIPPAGE
|
|
14
|
+
from wayfinder_paths.core.constants import DEFAULT_SLIPPAGE, ZERO_ADDRESS
|
|
15
15
|
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
16
16
|
from wayfinder_paths.core.services.base import Web3Service
|
|
17
17
|
from wayfinder_paths.core.settings import settings
|
|
@@ -108,6 +108,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
108
108
|
amount: str,
|
|
109
109
|
slippage: float | None = None,
|
|
110
110
|
wayfinder_fee: float | None = None,
|
|
111
|
+
preferred_providers: list[str] | None = None,
|
|
111
112
|
) -> tuple[bool, dict[str, Any] | str]:
|
|
112
113
|
"""
|
|
113
114
|
Get the best available quote for a swap operation.
|
|
@@ -122,6 +123,8 @@ class BRAPAdapter(BaseAdapter):
|
|
|
122
123
|
amount: Amount to swap (in smallest units)
|
|
123
124
|
slippage: Maximum slippage tolerance (optional)
|
|
124
125
|
wayfinder_fee: Wayfinder fee (optional)
|
|
126
|
+
preferred_providers: List of provider names in order of preference (optional).
|
|
127
|
+
If provided, selects first matching provider instead of "best".
|
|
125
128
|
|
|
126
129
|
Returns:
|
|
127
130
|
Tuple of (success, data) where data is best quote or error message
|
|
@@ -139,8 +142,27 @@ class BRAPAdapter(BaseAdapter):
|
|
|
139
142
|
wayfinder_fee=wayfinder_fee,
|
|
140
143
|
)
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
quotes_container = data.get("quotes", {})
|
|
146
|
+
# BRAP returns quotes in "all_quotes" not "quotes"
|
|
147
|
+
all_quotes = quotes_container.get("all_quotes", []) or quotes_container.get(
|
|
148
|
+
"quotes", []
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# If preferred providers specified, select by provider preference
|
|
152
|
+
if preferred_providers and all_quotes:
|
|
153
|
+
selected_quote = self._select_quote_by_provider(
|
|
154
|
+
all_quotes, preferred_providers
|
|
155
|
+
)
|
|
156
|
+
if selected_quote:
|
|
157
|
+
return (True, selected_quote)
|
|
158
|
+
# Fall through to best_quote if no preferred provider found
|
|
159
|
+
|
|
160
|
+
# Extract best quote from response - check nested quotes first, then top level
|
|
161
|
+
best_quote = (
|
|
162
|
+
quotes_container.get("best_quote")
|
|
163
|
+
or data.get("best_quote")
|
|
164
|
+
or data.get("best_route")
|
|
165
|
+
)
|
|
144
166
|
|
|
145
167
|
if not best_quote:
|
|
146
168
|
return (False, "No quotes available")
|
|
@@ -150,6 +172,58 @@ class BRAPAdapter(BaseAdapter):
|
|
|
150
172
|
self.logger.error(f"Error getting best quote: {e}")
|
|
151
173
|
return (False, str(e))
|
|
152
174
|
|
|
175
|
+
def _select_quote_by_provider(
|
|
176
|
+
self,
|
|
177
|
+
quotes: list[dict[str, Any]],
|
|
178
|
+
preferred_providers: list[str],
|
|
179
|
+
) -> dict[str, Any] | None:
|
|
180
|
+
"""
|
|
181
|
+
Select a quote from the list based on provider preference order.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
quotes: List of quote objects from BRAP API
|
|
185
|
+
preferred_providers: List of provider names in order of preference (case-insensitive)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The first matching quote, or None if no preferred provider found
|
|
189
|
+
"""
|
|
190
|
+
# Normalize preferred providers to lowercase for case-insensitive matching
|
|
191
|
+
preferred_lower = [p.lower() for p in preferred_providers]
|
|
192
|
+
|
|
193
|
+
# Build a map of provider name -> quotes
|
|
194
|
+
provider_quotes: dict[str, list[dict[str, Any]]] = {}
|
|
195
|
+
for quote in quotes:
|
|
196
|
+
# Provider name might be in different fields depending on BRAP response structure
|
|
197
|
+
provider = (
|
|
198
|
+
quote.get("provider")
|
|
199
|
+
or quote.get("provider_name")
|
|
200
|
+
or quote.get("source")
|
|
201
|
+
or quote.get("protocol")
|
|
202
|
+
or ""
|
|
203
|
+
).lower()
|
|
204
|
+
if provider:
|
|
205
|
+
if provider not in provider_quotes:
|
|
206
|
+
provider_quotes[provider] = []
|
|
207
|
+
provider_quotes[provider].append(quote)
|
|
208
|
+
|
|
209
|
+
# Select first matching provider in preference order
|
|
210
|
+
for pref in preferred_lower:
|
|
211
|
+
if pref in provider_quotes:
|
|
212
|
+
# Return the quote with highest output for this provider
|
|
213
|
+
provider_list = provider_quotes[pref]
|
|
214
|
+
best_for_provider = max(
|
|
215
|
+
provider_list, key=lambda q: int(q.get("output_amount", 0) or 0)
|
|
216
|
+
)
|
|
217
|
+
self.logger.info(f"Selected quote from preferred provider: {pref}")
|
|
218
|
+
return best_for_provider
|
|
219
|
+
|
|
220
|
+
# Log available providers for debugging
|
|
221
|
+
available = list(provider_quotes.keys())
|
|
222
|
+
self.logger.warning(
|
|
223
|
+
f"No preferred provider found. Wanted: {preferred_providers}, Available: {available}"
|
|
224
|
+
)
|
|
225
|
+
return None
|
|
226
|
+
|
|
153
227
|
async def calculate_swap_fees(
|
|
154
228
|
self,
|
|
155
229
|
from_token_address: str,
|
|
@@ -248,7 +322,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
248
322
|
)
|
|
249
323
|
|
|
250
324
|
quotes = data.get("quotes", {})
|
|
251
|
-
all_quotes = quotes.get("quotes", [])
|
|
325
|
+
all_quotes = quotes.get("all_quotes", []) or quotes.get("quotes", [])
|
|
252
326
|
best_quote = quotes.get("best_quote")
|
|
253
327
|
|
|
254
328
|
if not all_quotes:
|
|
@@ -291,9 +365,20 @@ class BRAPAdapter(BaseAdapter):
|
|
|
291
365
|
amount: str,
|
|
292
366
|
slippage: float = DEFAULT_SLIPPAGE,
|
|
293
367
|
strategy_name: str | None = None,
|
|
368
|
+
preferred_providers: list[str] | None = None,
|
|
294
369
|
) -> tuple[bool, Any]:
|
|
295
370
|
"""
|
|
296
371
|
Execute a swap by looking up token metadata via token IDs.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
from_token_id: Source token ID
|
|
375
|
+
to_token_id: Destination token ID
|
|
376
|
+
from_address: Wallet address
|
|
377
|
+
amount: Amount to swap (in smallest units)
|
|
378
|
+
slippage: Maximum slippage tolerance
|
|
379
|
+
strategy_name: Strategy name for ledger recording
|
|
380
|
+
preferred_providers: List of provider names in order of preference (e.g., ["enso", "aerodrome"]).
|
|
381
|
+
If provided, selects first matching provider instead of "best".
|
|
297
382
|
"""
|
|
298
383
|
from_token = await self.token_client.get_token_details(from_token_id)
|
|
299
384
|
if not from_token:
|
|
@@ -311,6 +396,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
311
396
|
to_address=from_address,
|
|
312
397
|
amount=amount,
|
|
313
398
|
slippage=slippage,
|
|
399
|
+
preferred_providers=preferred_providers,
|
|
314
400
|
)
|
|
315
401
|
if not success:
|
|
316
402
|
return (False, best_quote)
|
|
@@ -349,7 +435,14 @@ class BRAPAdapter(BaseAdapter):
|
|
|
349
435
|
or quote.get("inputAmount")
|
|
350
436
|
or transaction.get("value")
|
|
351
437
|
)
|
|
352
|
-
|
|
438
|
+
token_address = from_token.get("address")
|
|
439
|
+
token_address_l = str(token_address or "").lower()
|
|
440
|
+
is_native = token_address_l in {
|
|
441
|
+
"",
|
|
442
|
+
ZERO_ADDRESS.lower(),
|
|
443
|
+
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
|
444
|
+
}
|
|
445
|
+
if token_address and (not is_native) and spender and approve_amount:
|
|
353
446
|
approve_success, approve_response = await self._handle_token_approval(
|
|
354
447
|
chain=chain,
|
|
355
448
|
token_address=from_token.get("address"),
|
|
@@ -363,17 +456,36 @@ class BRAPAdapter(BaseAdapter):
|
|
|
363
456
|
broadcast_success, broadcast_response = await self._broadcast_transaction(
|
|
364
457
|
transaction
|
|
365
458
|
)
|
|
459
|
+
self.logger.info(
|
|
460
|
+
f"Swap broadcast result: success={broadcast_success}, "
|
|
461
|
+
f"response={broadcast_response}"
|
|
462
|
+
)
|
|
366
463
|
if not broadcast_success:
|
|
367
464
|
return (False, broadcast_response)
|
|
368
465
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
466
|
+
# Record the swap operation in ledger - but don't let ledger errors fail the swap
|
|
467
|
+
# since the on-chain transaction already succeeded
|
|
468
|
+
try:
|
|
469
|
+
ledger_record = await self._record_swap_operation(
|
|
470
|
+
from_token=from_token,
|
|
471
|
+
to_token=to_token,
|
|
472
|
+
wallet_address=from_address,
|
|
473
|
+
quote=quote,
|
|
474
|
+
broadcast_response=broadcast_response,
|
|
475
|
+
strategy_name=strategy_name,
|
|
476
|
+
)
|
|
477
|
+
except Exception as e:
|
|
478
|
+
self.logger.warning(
|
|
479
|
+
f"Ledger recording failed (swap succeeded on-chain): {e}"
|
|
480
|
+
)
|
|
481
|
+
# Return the quote with output amount so caller can proceed
|
|
482
|
+
ledger_record = {
|
|
483
|
+
"to_amount": quote.get("output_amount"),
|
|
484
|
+
"from_amount": quote.get("input_amount"),
|
|
485
|
+
"tx_hash": broadcast_response.get("tx_hash")
|
|
486
|
+
if isinstance(broadcast_response, dict)
|
|
487
|
+
else None,
|
|
488
|
+
}
|
|
377
489
|
return (True, ledger_record)
|
|
378
490
|
|
|
379
491
|
async def get_bridge_quote(
|
|
@@ -581,10 +693,10 @@ class BRAPAdapter(BaseAdapter):
|
|
|
581
693
|
)
|
|
582
694
|
if not build_success:
|
|
583
695
|
return False, approve_tx
|
|
584
|
-
return await self._broadcast_transaction(approve_tx)
|
|
696
|
+
return await self._broadcast_transaction(approve_tx, confirmations=2)
|
|
585
697
|
|
|
586
698
|
async def _broadcast_transaction(
|
|
587
|
-
self, transaction: dict[str, Any]
|
|
699
|
+
self, transaction: dict[str, Any], confirmations: int = 0
|
|
588
700
|
) -> tuple[bool, Any]:
|
|
589
701
|
if getattr(settings, "DRY_RUN", False):
|
|
590
702
|
return True, {"dry_run": True, "transaction": transaction}
|
|
@@ -592,6 +704,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
592
704
|
transaction,
|
|
593
705
|
wait_for_receipt=True,
|
|
594
706
|
timeout=DEFAULT_TRANSACTION_TIMEOUT,
|
|
707
|
+
confirmations=confirmations,
|
|
595
708
|
)
|
|
596
709
|
|
|
597
710
|
async def _record_swap_operation(
|
|
@@ -618,16 +731,19 @@ class BRAPAdapter(BaseAdapter):
|
|
|
618
731
|
response = broadcast_response if isinstance(broadcast_response, dict) else {}
|
|
619
732
|
operation_data = SWAP(
|
|
620
733
|
adapter=self.adapter_type,
|
|
621
|
-
from_token_id=from_token.get("id"),
|
|
622
|
-
to_token_id=to_token.get("id"),
|
|
623
|
-
from_amount=quote.get("input_amount"),
|
|
624
|
-
to_amount=quote.get("output_amount"),
|
|
734
|
+
from_token_id=str(from_token.get("id")),
|
|
735
|
+
to_token_id=str(to_token.get("id")),
|
|
736
|
+
from_amount=str(quote.get("input_amount")),
|
|
737
|
+
to_amount=str(quote.get("output_amount")),
|
|
625
738
|
from_amount_usd=from_amount_usd or 0,
|
|
626
739
|
to_amount_usd=to_amount_usd or 0,
|
|
627
|
-
transaction_hash=response.get("
|
|
628
|
-
|
|
740
|
+
transaction_hash=response.get("tx_hash")
|
|
741
|
+
or response.get("transaction_hash"),
|
|
742
|
+
transaction_chain_id=from_token.get("chain_id")
|
|
743
|
+
or (from_token.get("chain") or {}).get("id"),
|
|
629
744
|
transaction_status=response.get("transaction_status"),
|
|
630
|
-
|
|
745
|
+
# Don't pass raw receipt - it contains HexBytes that can't be JSON serialized
|
|
746
|
+
transaction_receipt=None,
|
|
631
747
|
)
|
|
632
748
|
|
|
633
749
|
try:
|
|
@@ -653,7 +769,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
653
769
|
if raw_amount is None:
|
|
654
770
|
return None
|
|
655
771
|
success, price_data = await self.token_adapter.get_token_price(
|
|
656
|
-
token_info.get("
|
|
772
|
+
token_info.get("token_id")
|
|
657
773
|
)
|
|
658
774
|
if not success or not price_data:
|
|
659
775
|
return None
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Moonwell Adapter
|
|
2
|
+
|
|
3
|
+
Adapter for interacting with the [Moonwell](https://moonwell.fi/) lending protocol on Base chain.
|
|
4
|
+
|
|
5
|
+
- Entrypoint: `adapters.moonwell_adapter.adapter.MoonwellAdapter`
|
|
6
|
+
- Tests: `test_adapter.py`
|
|
7
|
+
|
|
8
|
+
## Capabilities
|
|
9
|
+
|
|
10
|
+
The adapter provides the following capabilities:
|
|
11
|
+
- Lending: Supply and withdraw tokens
|
|
12
|
+
- Borrowing: Borrow and repay tokens
|
|
13
|
+
- Collateral management: Enable/disable markets as collateral
|
|
14
|
+
- Rewards: Claim WELL token rewards
|
|
15
|
+
- Position & market queries: Get balances, APYs, and liquidity info
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
The MoonwellAdapter provides functionality for:
|
|
20
|
+
- **Lending**: Supply tokens to earn yield
|
|
21
|
+
- **Borrowing**: Borrow against collateral
|
|
22
|
+
- **Collateral Management**: Enable/disable markets as collateral
|
|
23
|
+
- **Rewards**: Claim WELL token rewards
|
|
24
|
+
- **Position Queries**: Get balances, APYs, and liquidity info
|
|
25
|
+
|
|
26
|
+
## Supported Markets (Base Chain)
|
|
27
|
+
|
|
28
|
+
| Token | mToken Address | Underlying Address |
|
|
29
|
+
|-------|----------------|-------------------|
|
|
30
|
+
| USDC | `0xEdc817A28E8B93B03976FBd4a3dDBc9f7D176c22` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
|
|
31
|
+
| WETH | `0x628ff693426583D9a7FB391E54366292F509D457` | `0x4200000000000000000000000000000000000006` |
|
|
32
|
+
| wstETH | `0x627Fe393Bc6EdDA28e99AE648fD6fF362514304b` | `0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452` |
|
|
33
|
+
|
|
34
|
+
## Protocol Addresses (Base Chain)
|
|
35
|
+
|
|
36
|
+
- **Comptroller**: `0xfbb21d0380bee3312b33c4353c8936a0f13ef26c`
|
|
37
|
+
- **Reward Distributor**: `0xe9005b078701e2a0948d2eac43010d35870ad9d2`
|
|
38
|
+
- **WELL Token**: `0xA88594D404727625A9437C3f886C7643872296AE`
|
|
39
|
+
|
|
40
|
+
## Construction
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
|
|
44
|
+
from wayfinder_paths.adapters.moonwell_adapter import MoonwellAdapter
|
|
45
|
+
|
|
46
|
+
config = {
|
|
47
|
+
"strategy_wallet": {"address": "0x...your_wallet..."},
|
|
48
|
+
"moonwell_adapter": {
|
|
49
|
+
"chain_id": 8453, # Base chain (default)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
web3_service = DefaultWeb3Service(config)
|
|
53
|
+
adapter = MoonwellAdapter(config=config, web3_service=web3_service)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`web3_service` is required so the adapter can share the same wallet provider as the rest of the strategy.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# Supply tokens
|
|
62
|
+
await adapter.lend(
|
|
63
|
+
mtoken="0xEdc817A28E8B93B03976FBd4a3dDBc9f7D176c22", # mUSDC
|
|
64
|
+
underlying_token="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
|
|
65
|
+
amount=1000 * 10**6, # 1000 USDC
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Enable as collateral
|
|
69
|
+
await adapter.set_collateral(
|
|
70
|
+
mtoken="0x627Fe393Bc6EdDA28e99AE648fD6fF362514304b", # mwstETH
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Borrow
|
|
74
|
+
await adapter.borrow(
|
|
75
|
+
mtoken="0x628ff693426583D9a7FB391E54366292F509D457", # mWETH
|
|
76
|
+
amount=10**17, # 0.1 WETH
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Get position info
|
|
80
|
+
success, position = await adapter.get_pos(
|
|
81
|
+
mtoken="0xEdc817A28E8B93B03976FBd4a3dDBc9f7D176c22",
|
|
82
|
+
)
|
|
83
|
+
# Returns: {mtoken_balance, underlying_balance, borrow_balance, exchange_rate, balances}
|
|
84
|
+
|
|
85
|
+
# Get collateral factor
|
|
86
|
+
success, cf = await adapter.get_collateral_factor(
|
|
87
|
+
mtoken="0x627Fe393Bc6EdDA28e99AE648fD6fF362514304b",
|
|
88
|
+
)
|
|
89
|
+
# Returns: 0.75 (75% LTV)
|
|
90
|
+
|
|
91
|
+
# Get APY
|
|
92
|
+
success, apy = await adapter.get_apy(
|
|
93
|
+
mtoken="0xEdc817A28E8B93B03976FBd4a3dDBc9f7D176c22",
|
|
94
|
+
apy_type="supply", # or "borrow"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Claim rewards
|
|
98
|
+
await adapter.claim_rewards()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## API Reference
|
|
102
|
+
|
|
103
|
+
### Lending Operations
|
|
104
|
+
|
|
105
|
+
| Method | Description |
|
|
106
|
+
|--------|-------------|
|
|
107
|
+
| `lend(mtoken, underlying_token, amount)` | Supply tokens to earn yield |
|
|
108
|
+
| `unlend(mtoken, amount)` | Withdraw by redeeming mTokens |
|
|
109
|
+
|
|
110
|
+
### Borrowing Operations
|
|
111
|
+
|
|
112
|
+
| Method | Description |
|
|
113
|
+
|--------|-------------|
|
|
114
|
+
| `borrow(mtoken, amount)` | Borrow underlying tokens |
|
|
115
|
+
| `repay(mtoken, underlying_token, amount)` | Repay borrowed tokens |
|
|
116
|
+
|
|
117
|
+
### Collateral Management
|
|
118
|
+
|
|
119
|
+
| Method | Description |
|
|
120
|
+
|--------|-------------|
|
|
121
|
+
| `set_collateral(mtoken)` | Enable market as collateral (enterMarkets) |
|
|
122
|
+
| `remove_collateral(mtoken)` | Disable market as collateral (exitMarket) |
|
|
123
|
+
|
|
124
|
+
### Position & Market Data
|
|
125
|
+
|
|
126
|
+
| Method | Description |
|
|
127
|
+
|--------|-------------|
|
|
128
|
+
| `get_pos(mtoken, account)` | Get position data (balances, debt, rewards) |
|
|
129
|
+
| `get_collateral_factor(mtoken)` | Get collateral factor (LTV) |
|
|
130
|
+
| `get_apy(mtoken, apy_type)` | Get supply or borrow APY |
|
|
131
|
+
| `get_borrowable_amount(account)` | Get max borrowable in USD |
|
|
132
|
+
| `max_withdrawable_mtoken(mtoken, account)` | Calculate safe withdrawal amount via binary search |
|
|
133
|
+
|
|
134
|
+
### Rewards
|
|
135
|
+
|
|
136
|
+
| Method | Description |
|
|
137
|
+
|--------|-------------|
|
|
138
|
+
| `claim_rewards(min_rewards_usd)` | Claim WELL rewards (skips if below threshold) |
|
|
139
|
+
|
|
140
|
+
### Utilities
|
|
141
|
+
|
|
142
|
+
| Method | Description |
|
|
143
|
+
|--------|-------------|
|
|
144
|
+
| `wrap_eth(amount)` | Wrap ETH to WETH |
|
|
145
|
+
|
|
146
|
+
All methods return `(success: bool, payload: Any)` tuples. On failure the payload is an error string.
|
|
147
|
+
|
|
148
|
+
## Testing
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
poetry run pytest wayfinder_paths/adapters/moonwell_adapter/ -v
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Configuration
|
|
155
|
+
|
|
156
|
+
The adapter can be configured via the `moonwell_adapter` config key:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
config = {
|
|
160
|
+
"strategy_wallet": {"address": "0x..."},
|
|
161
|
+
"moonwell_adapter": {
|
|
162
|
+
"chain_id": 8453, # Chain ID (default: Base)
|
|
163
|
+
"comptroller": "0x...", # Override comptroller address
|
|
164
|
+
"reward_distributor": "0x...", # Override reward distributor
|
|
165
|
+
"m_usdc": "0x...", # Override mUSDC address
|
|
166
|
+
"m_weth": "0x...", # Override mWETH address
|
|
167
|
+
"m_wsteth": "0x...", # Override mwstETH address
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Error handling
|
|
173
|
+
|
|
174
|
+
Any exception raised by the underlying web3 calls is caught and returned as a `(False, "message")` tuple. The inherited `health_check()` method reports adapter status, making it safe to call from `Strategy.health_check`.
|