wayfinder-paths 0.1.13__py3-none-any.whl → 0.1.15__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/adapters/balance_adapter/README.md +13 -14
- wayfinder_paths/adapters/balance_adapter/adapter.py +73 -32
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
- wayfinder_paths/adapters/brap_adapter/README.md +11 -16
- wayfinder_paths/adapters/brap_adapter/adapter.py +144 -78
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +127 -65
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +30 -14
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +121 -67
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +332 -9
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +13 -13
- wayfinder_paths/adapters/pool_adapter/README.md +9 -10
- wayfinder_paths/adapters/pool_adapter/adapter.py +9 -10
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
- wayfinder_paths/adapters/token_adapter/README.md +2 -14
- wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
- wayfinder_paths/adapters/token_adapter/examples.json +4 -8
- wayfinder_paths/adapters/token_adapter/test_adapter.py +9 -7
- wayfinder_paths/core/clients/BRAPClient.py +102 -61
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +125 -64
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +122 -48
- wayfinder_paths/core/clients/TokenClient.py +91 -36
- wayfinder_paths/core/clients/WalletClient.py +26 -56
- wayfinder_paths/core/clients/WayfinderClient.py +28 -160
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -46
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/constants/erc20_abi.py +0 -11
- wayfinder_paths/core/engine/StrategyJob.py +10 -56
- wayfinder_paths/core/services/base.py +1 -0
- wayfinder_paths/core/services/local_evm_txn.py +25 -9
- wayfinder_paths/core/services/local_token_txn.py +2 -6
- wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
- wayfinder_paths/core/strategies/Strategy.py +16 -4
- wayfinder_paths/core/utils/evm_helpers.py +2 -9
- wayfinder_paths/policies/erc20.py +1 -1
- wayfinder_paths/run_strategy.py +13 -19
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +77 -11
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +6 -6
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -5
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2246 -1279
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +276 -109
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +1 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +153 -56
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +16 -12
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +3 -3
- wayfinder_paths/templates/strategy/test_strategy.py +3 -2
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/METADATA +14 -49
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/RECORD +59 -60
- wayfinder_paths/abis/generic/erc20.json +0 -383
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/WHEEL +0 -0
|
@@ -16,6 +16,7 @@ from eth_utils import to_checksum_address
|
|
|
16
16
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
17
17
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
18
18
|
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
19
|
+
from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
|
|
19
20
|
from wayfinder_paths.core.constants.moonwell_abi import (
|
|
20
21
|
COMPTROLLER_ABI,
|
|
21
22
|
MTOKEN_ABI,
|
|
@@ -57,6 +58,11 @@ CF_CACHE_TTL = 3600
|
|
|
57
58
|
DEFAULT_MAX_RETRIES = 5
|
|
58
59
|
DEFAULT_BASE_DELAY = 3.0 # seconds
|
|
59
60
|
|
|
61
|
+
# Compound-style Failure(uint256,uint256,uint256) topic0
|
|
62
|
+
FAILURE_EVENT_TOPIC0 = (
|
|
63
|
+
"0x45b96fe442630264581b197e84bbada861235052c5a1aadfff9ea4e40a969aa0"
|
|
64
|
+
)
|
|
65
|
+
|
|
60
66
|
|
|
61
67
|
def _is_rate_limit_error(error: Exception | str) -> bool:
|
|
62
68
|
"""Check if an error is a rate limit (429) error."""
|
|
@@ -163,6 +169,79 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
163
169
|
# Public API - Lending Operations #
|
|
164
170
|
# ------------------------------------------------------------------ #
|
|
165
171
|
|
|
172
|
+
def _tx_pinned_block(self, result: Any) -> int | None:
|
|
173
|
+
if isinstance(result, dict):
|
|
174
|
+
return (
|
|
175
|
+
result.get("confirmed_block_number")
|
|
176
|
+
or result.get("block_number")
|
|
177
|
+
or (result.get("receipt", {}) or {}).get("blockNumber")
|
|
178
|
+
)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def _as_bytes(self, value: Any) -> bytes | None:
|
|
182
|
+
if value is None:
|
|
183
|
+
return None
|
|
184
|
+
if isinstance(value, (bytes, bytearray)):
|
|
185
|
+
return bytes(value)
|
|
186
|
+
if hasattr(value, "hex"):
|
|
187
|
+
try:
|
|
188
|
+
hex_str = value.hex()
|
|
189
|
+
if isinstance(hex_str, str):
|
|
190
|
+
if hex_str.startswith("0x"):
|
|
191
|
+
return bytes.fromhex(hex_str[2:])
|
|
192
|
+
return bytes.fromhex(hex_str)
|
|
193
|
+
except Exception:
|
|
194
|
+
return None
|
|
195
|
+
if isinstance(value, str):
|
|
196
|
+
v = value
|
|
197
|
+
if v.startswith("0x"):
|
|
198
|
+
v = v[2:]
|
|
199
|
+
try:
|
|
200
|
+
return bytes.fromhex(v)
|
|
201
|
+
except Exception:
|
|
202
|
+
return None
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
def _failure_event_details(
|
|
206
|
+
self, result: Any, contract_address: str
|
|
207
|
+
) -> dict[str, int] | None:
|
|
208
|
+
if not isinstance(result, dict):
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
receipt = (
|
|
212
|
+
result.get("receipt") if isinstance(result.get("receipt"), dict) else {}
|
|
213
|
+
)
|
|
214
|
+
logs = receipt.get("logs") if isinstance(receipt, dict) else None
|
|
215
|
+
if not isinstance(logs, list):
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
addr_l = str(contract_address or "").lower()
|
|
219
|
+
for log in logs:
|
|
220
|
+
if not isinstance(log, dict):
|
|
221
|
+
continue
|
|
222
|
+
if str(log.get("address") or "").lower() != addr_l:
|
|
223
|
+
continue
|
|
224
|
+
topics = log.get("topics") or []
|
|
225
|
+
if not topics:
|
|
226
|
+
continue
|
|
227
|
+
topic0_bytes = self._as_bytes(topics[0])
|
|
228
|
+
if not topic0_bytes:
|
|
229
|
+
continue
|
|
230
|
+
if topic0_bytes.hex() != FAILURE_EVENT_TOPIC0.lower().removeprefix("0x"):
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
data_b = self._as_bytes(log.get("data"))
|
|
234
|
+
if not data_b or len(data_b) < 96:
|
|
235
|
+
return {"error": 0, "info": 0, "detail": 0}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
"error": int.from_bytes(data_b[0:32], "big"),
|
|
239
|
+
"info": int.from_bytes(data_b[32:64], "big"),
|
|
240
|
+
"detail": int.from_bytes(data_b[64:96], "big"),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return None
|
|
244
|
+
|
|
166
245
|
async def lend(
|
|
167
246
|
self,
|
|
168
247
|
*,
|
|
@@ -170,7 +249,11 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
170
249
|
underlying_token: str,
|
|
171
250
|
amount: int,
|
|
172
251
|
) -> tuple[bool, Any]:
|
|
173
|
-
"""Supply tokens to Moonwell by minting mTokens.
|
|
252
|
+
"""Supply tokens to Moonwell by minting mTokens.
|
|
253
|
+
|
|
254
|
+
Note: mint() returns an error code and can emit Failure without reverting.
|
|
255
|
+
We verify success by checking that the mToken balance increased.
|
|
256
|
+
"""
|
|
174
257
|
strategy = self._strategy_address()
|
|
175
258
|
amount = int(amount)
|
|
176
259
|
if amount <= 0:
|
|
@@ -179,6 +262,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
179
262
|
mtoken = self._checksum(mtoken)
|
|
180
263
|
underlying_token = self._checksum(underlying_token)
|
|
181
264
|
|
|
265
|
+
mtoken_bal_before = None
|
|
266
|
+
if self.web3 and not self.simulation:
|
|
267
|
+
try:
|
|
268
|
+
web3 = self.web3.get_web3(self.chain_id)
|
|
269
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
270
|
+
mtoken_bal_before = await mtoken_contract.functions.balanceOf(
|
|
271
|
+
strategy
|
|
272
|
+
).call()
|
|
273
|
+
except Exception:
|
|
274
|
+
mtoken_bal_before = None
|
|
275
|
+
|
|
182
276
|
# Approve mToken to spend underlying tokens
|
|
183
277
|
approved = await self._ensure_allowance(
|
|
184
278
|
token_address=underlying_token,
|
|
@@ -197,7 +291,41 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
197
291
|
args=[amount],
|
|
198
292
|
from_address=strategy,
|
|
199
293
|
)
|
|
200
|
-
|
|
294
|
+
result = await self._execute(tx)
|
|
295
|
+
|
|
296
|
+
if not result[0] or not self.web3 or self.simulation:
|
|
297
|
+
return result
|
|
298
|
+
|
|
299
|
+
if isinstance(result[1], dict):
|
|
300
|
+
failure = self._failure_event_details(result[1], mtoken)
|
|
301
|
+
if failure is not None:
|
|
302
|
+
return (
|
|
303
|
+
False,
|
|
304
|
+
f"Mint failed (Failure event): error={failure['error']} info={failure['info']} detail={failure['detail']}",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if mtoken_bal_before is None:
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
pinned_block = self._tx_pinned_block(result[1])
|
|
312
|
+
block_id = pinned_block if pinned_block is not None else "latest"
|
|
313
|
+
|
|
314
|
+
web3 = self.web3.get_web3(self.chain_id)
|
|
315
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
316
|
+
mtoken_bal_after = await mtoken_contract.functions.balanceOf(strategy).call(
|
|
317
|
+
block_identifier=block_id
|
|
318
|
+
)
|
|
319
|
+
if int(mtoken_bal_after) <= int(mtoken_bal_before):
|
|
320
|
+
return (
|
|
321
|
+
False,
|
|
322
|
+
f"Mint verification failed: mToken balance did not increase (before={mtoken_bal_before}, after={mtoken_bal_after})",
|
|
323
|
+
)
|
|
324
|
+
except Exception:
|
|
325
|
+
# If verification fails due to RPC/ABI issues, keep original result.
|
|
326
|
+
return result
|
|
327
|
+
|
|
328
|
+
return result
|
|
201
329
|
|
|
202
330
|
async def unlend(
|
|
203
331
|
self,
|
|
@@ -205,7 +333,13 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
205
333
|
mtoken: str,
|
|
206
334
|
amount: int,
|
|
207
335
|
) -> tuple[bool, Any]:
|
|
208
|
-
"""Withdraw tokens from Moonwell by redeeming mTokens.
|
|
336
|
+
"""Withdraw tokens from Moonwell by redeeming mTokens.
|
|
337
|
+
|
|
338
|
+
Note: redeem() returns an error code and can emit Failure without reverting.
|
|
339
|
+
We verify success by checking that either:
|
|
340
|
+
- underlying wallet balance increased, or
|
|
341
|
+
- mToken wallet balance decreased.
|
|
342
|
+
"""
|
|
209
343
|
strategy = self._strategy_address()
|
|
210
344
|
amount = int(amount)
|
|
211
345
|
if amount <= 0:
|
|
@@ -213,6 +347,38 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
213
347
|
|
|
214
348
|
mtoken = self._checksum(mtoken)
|
|
215
349
|
|
|
350
|
+
mtoken_bal_before = None
|
|
351
|
+
underlying_addr = None
|
|
352
|
+
underlying_bal_before = None
|
|
353
|
+
|
|
354
|
+
if self.web3 and not self.simulation:
|
|
355
|
+
try:
|
|
356
|
+
web3 = self.web3.get_web3(self.chain_id)
|
|
357
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
358
|
+
|
|
359
|
+
# Snapshot balances for verification.
|
|
360
|
+
mtoken_bal_before = await mtoken_contract.functions.balanceOf(
|
|
361
|
+
strategy
|
|
362
|
+
).call()
|
|
363
|
+
try:
|
|
364
|
+
underlying_addr = (
|
|
365
|
+
await mtoken_contract.functions.underlying().call()
|
|
366
|
+
)
|
|
367
|
+
except Exception:
|
|
368
|
+
underlying_addr = None
|
|
369
|
+
if underlying_addr:
|
|
370
|
+
underlying_contract = web3.eth.contract(
|
|
371
|
+
address=to_checksum_address(underlying_addr),
|
|
372
|
+
abi=ERC20_ABI,
|
|
373
|
+
)
|
|
374
|
+
underlying_bal_before = (
|
|
375
|
+
await underlying_contract.functions.balanceOf(strategy).call()
|
|
376
|
+
)
|
|
377
|
+
except Exception:
|
|
378
|
+
mtoken_bal_before = None
|
|
379
|
+
underlying_addr = None
|
|
380
|
+
underlying_bal_before = None
|
|
381
|
+
|
|
216
382
|
# Redeem mTokens for underlying
|
|
217
383
|
tx = await self._encode_call(
|
|
218
384
|
target=mtoken,
|
|
@@ -221,7 +387,60 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
221
387
|
args=[amount],
|
|
222
388
|
from_address=strategy,
|
|
223
389
|
)
|
|
224
|
-
|
|
390
|
+
result = await self._execute(tx)
|
|
391
|
+
|
|
392
|
+
if not result[0] or not self.web3 or self.simulation:
|
|
393
|
+
return result
|
|
394
|
+
|
|
395
|
+
if isinstance(result[1], dict):
|
|
396
|
+
failure = self._failure_event_details(result[1], mtoken)
|
|
397
|
+
if failure is not None:
|
|
398
|
+
return (
|
|
399
|
+
False,
|
|
400
|
+
f"Redeem failed (Failure event): error={failure['error']} info={failure['info']} detail={failure['detail']}",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if mtoken_bal_before is None:
|
|
404
|
+
return result
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
pinned_block = self._tx_pinned_block(result[1])
|
|
408
|
+
block_id = pinned_block if pinned_block is not None else "latest"
|
|
409
|
+
|
|
410
|
+
web3 = self.web3.get_web3(self.chain_id)
|
|
411
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
412
|
+
mtoken_bal_after = await mtoken_contract.functions.balanceOf(strategy).call(
|
|
413
|
+
block_identifier=block_id
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
underlying_bal_after = None
|
|
417
|
+
if underlying_addr:
|
|
418
|
+
underlying_contract = web3.eth.contract(
|
|
419
|
+
address=to_checksum_address(underlying_addr),
|
|
420
|
+
abi=ERC20_ABI,
|
|
421
|
+
)
|
|
422
|
+
underlying_bal_after = await underlying_contract.functions.balanceOf(
|
|
423
|
+
strategy
|
|
424
|
+
).call(block_identifier=block_id)
|
|
425
|
+
|
|
426
|
+
mtoken_decreased = int(mtoken_bal_after) < int(mtoken_bal_before)
|
|
427
|
+
underlying_increased = (
|
|
428
|
+
underlying_bal_before is not None
|
|
429
|
+
and underlying_bal_after is not None
|
|
430
|
+
and int(underlying_bal_after) > int(underlying_bal_before)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
if not mtoken_decreased and not underlying_increased:
|
|
434
|
+
return (
|
|
435
|
+
False,
|
|
436
|
+
"Redeem verification failed: no observed balance change "
|
|
437
|
+
f"(mtoken before={mtoken_bal_before}, after={mtoken_bal_after}; "
|
|
438
|
+
f"underlying before={underlying_bal_before}, after={underlying_bal_after})",
|
|
439
|
+
)
|
|
440
|
+
except Exception:
|
|
441
|
+
return result
|
|
442
|
+
|
|
443
|
+
return result
|
|
225
444
|
|
|
226
445
|
# ------------------------------------------------------------------ #
|
|
227
446
|
# Public API - Borrowing Operations #
|
|
@@ -292,11 +511,20 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
292
511
|
# Verify the borrow actually succeeded by checking balance increased
|
|
293
512
|
if self.web3:
|
|
294
513
|
try:
|
|
514
|
+
pinned_block = None
|
|
515
|
+
if isinstance(result[1], dict):
|
|
516
|
+
pinned_block = (
|
|
517
|
+
result[1].get("confirmed_block_number")
|
|
518
|
+
or result[1].get("block_number")
|
|
519
|
+
or (result[1].get("receipt", {}) or {}).get("blockNumber")
|
|
520
|
+
)
|
|
521
|
+
block_id = pinned_block if pinned_block is not None else "latest"
|
|
522
|
+
|
|
295
523
|
web3 = self.web3.get_web3(self.chain_id)
|
|
296
524
|
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
297
525
|
borrow_after = await mtoken_contract.functions.borrowBalanceStored(
|
|
298
526
|
strategy
|
|
299
|
-
).call()
|
|
527
|
+
).call(block_identifier=block_id)
|
|
300
528
|
|
|
301
529
|
# Borrow balance should have increased by approximately the amount
|
|
302
530
|
# Allow for some interest accrual
|
|
@@ -349,6 +577,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
349
577
|
mtoken = self._checksum(mtoken)
|
|
350
578
|
underlying_token = self._checksum(underlying_token)
|
|
351
579
|
|
|
580
|
+
borrow_before = None
|
|
581
|
+
if self.web3 and not self.simulation:
|
|
582
|
+
try:
|
|
583
|
+
web3 = self.web3.get_web3(self.chain_id)
|
|
584
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
585
|
+
borrow_before = await mtoken_contract.functions.borrowBalanceStored(
|
|
586
|
+
strategy
|
|
587
|
+
).call()
|
|
588
|
+
except Exception:
|
|
589
|
+
borrow_before = None
|
|
590
|
+
|
|
352
591
|
# Approve mToken to spend underlying tokens for repayment
|
|
353
592
|
# When repay_full=True, approve the amount we have, Moonwell will use only what's needed
|
|
354
593
|
approved = await self._ensure_allowance(
|
|
@@ -370,7 +609,49 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
370
609
|
args=[repay_amount],
|
|
371
610
|
from_address=strategy,
|
|
372
611
|
)
|
|
373
|
-
|
|
612
|
+
result = await self._execute(tx)
|
|
613
|
+
|
|
614
|
+
if not result[0] or not self.web3 or self.simulation:
|
|
615
|
+
return result
|
|
616
|
+
|
|
617
|
+
if isinstance(result[1], dict):
|
|
618
|
+
failure = self._failure_event_details(result[1], mtoken)
|
|
619
|
+
if failure is not None:
|
|
620
|
+
return (
|
|
621
|
+
False,
|
|
622
|
+
f"Repay failed (Failure event): error={failure['error']} info={failure['info']} detail={failure['detail']}",
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
if borrow_before is None or int(borrow_before) <= 0:
|
|
626
|
+
return result
|
|
627
|
+
|
|
628
|
+
try:
|
|
629
|
+
pinned_block = self._tx_pinned_block(result[1])
|
|
630
|
+
block_id = pinned_block if pinned_block is not None else "latest"
|
|
631
|
+
|
|
632
|
+
web3 = self.web3.get_web3(self.chain_id)
|
|
633
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
634
|
+
borrow_after = await mtoken_contract.functions.borrowBalanceStored(
|
|
635
|
+
strategy
|
|
636
|
+
).call(block_identifier=block_id)
|
|
637
|
+
|
|
638
|
+
if repay_full:
|
|
639
|
+
# Full repayment should clear the borrow balance (allow 1 wei dust).
|
|
640
|
+
if int(borrow_after) > 1:
|
|
641
|
+
return (
|
|
642
|
+
False,
|
|
643
|
+
f"Repay verification failed: repay_full did not clear debt (before={borrow_before}, after={borrow_after})",
|
|
644
|
+
)
|
|
645
|
+
else:
|
|
646
|
+
if int(borrow_after) >= int(borrow_before):
|
|
647
|
+
return (
|
|
648
|
+
False,
|
|
649
|
+
f"Repay verification failed: borrow balance did not decrease (before={borrow_before}, after={borrow_after})",
|
|
650
|
+
)
|
|
651
|
+
except Exception:
|
|
652
|
+
return result
|
|
653
|
+
|
|
654
|
+
return result
|
|
374
655
|
|
|
375
656
|
# ------------------------------------------------------------------ #
|
|
376
657
|
# Public API - Collateral Management #
|
|
@@ -404,13 +685,22 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
404
685
|
# Verify the market was actually entered
|
|
405
686
|
if self.web3:
|
|
406
687
|
try:
|
|
688
|
+
pinned_block = None
|
|
689
|
+
if isinstance(result[1], dict):
|
|
690
|
+
pinned_block = (
|
|
691
|
+
result[1].get("confirmed_block_number")
|
|
692
|
+
or result[1].get("block_number")
|
|
693
|
+
or (result[1].get("receipt", {}) or {}).get("blockNumber")
|
|
694
|
+
)
|
|
695
|
+
block_id = pinned_block if pinned_block is not None else "latest"
|
|
696
|
+
|
|
407
697
|
web3 = self.web3.get_web3(self.chain_id)
|
|
408
698
|
comptroller = web3.eth.contract(
|
|
409
699
|
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
410
700
|
)
|
|
411
701
|
is_member = await comptroller.functions.checkMembership(
|
|
412
702
|
strategy, mtoken
|
|
413
|
-
).call()
|
|
703
|
+
).call(block_identifier=block_id)
|
|
414
704
|
|
|
415
705
|
if not is_member:
|
|
416
706
|
from loguru import logger
|
|
@@ -430,6 +720,31 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
430
720
|
|
|
431
721
|
return result
|
|
432
722
|
|
|
723
|
+
async def is_market_entered(
|
|
724
|
+
self,
|
|
725
|
+
*,
|
|
726
|
+
mtoken: str,
|
|
727
|
+
account: str | None = None,
|
|
728
|
+
) -> tuple[bool, bool | str]:
|
|
729
|
+
"""Check whether an account has entered a given market (as collateral / borrowing market)."""
|
|
730
|
+
if self.simulation:
|
|
731
|
+
return True, True
|
|
732
|
+
if not self.web3:
|
|
733
|
+
return False, "web3 service not configured"
|
|
734
|
+
|
|
735
|
+
try:
|
|
736
|
+
acct = self._checksum(account) if account else self._strategy_address()
|
|
737
|
+
mtoken = self._checksum(mtoken)
|
|
738
|
+
|
|
739
|
+
web3 = self.web3.get_web3(self.chain_id)
|
|
740
|
+
comptroller = web3.eth.contract(
|
|
741
|
+
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
742
|
+
)
|
|
743
|
+
is_member = await comptroller.functions.checkMembership(acct, mtoken).call()
|
|
744
|
+
return True, bool(is_member)
|
|
745
|
+
except Exception as exc:
|
|
746
|
+
return False, str(exc)
|
|
747
|
+
|
|
433
748
|
async def remove_collateral(
|
|
434
749
|
self,
|
|
435
750
|
*,
|
|
@@ -1107,9 +1422,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1107
1422
|
|
|
1108
1423
|
result = await self._broadcast_transaction(approve_tx)
|
|
1109
1424
|
|
|
1110
|
-
# Small delay after approval to ensure state is propagated
|
|
1425
|
+
# Small delay after approval to ensure state is propagated on providers/chains
|
|
1426
|
+
# where we don't wait for additional confirmations by default.
|
|
1111
1427
|
if result[0]:
|
|
1112
|
-
|
|
1428
|
+
confirmations = 0
|
|
1429
|
+
if isinstance(result[1], dict):
|
|
1430
|
+
try:
|
|
1431
|
+
confirmations = int(result[1].get("confirmations") or 0)
|
|
1432
|
+
except (TypeError, ValueError):
|
|
1433
|
+
confirmations = 0
|
|
1434
|
+
if confirmations == 0:
|
|
1435
|
+
await asyncio.sleep(1.0)
|
|
1113
1436
|
|
|
1114
1437
|
return result
|
|
1115
1438
|
|
|
@@ -103,7 +103,7 @@ class TestMoonwellAdapter:
|
|
|
103
103
|
amount=10**6,
|
|
104
104
|
)
|
|
105
105
|
|
|
106
|
-
assert success
|
|
106
|
+
assert success
|
|
107
107
|
assert "simulation" in result
|
|
108
108
|
|
|
109
109
|
@pytest.mark.asyncio
|
|
@@ -137,7 +137,7 @@ class TestMoonwellAdapter:
|
|
|
137
137
|
amount=10**8,
|
|
138
138
|
)
|
|
139
139
|
|
|
140
|
-
assert success
|
|
140
|
+
assert success
|
|
141
141
|
assert "simulation" in result
|
|
142
142
|
|
|
143
143
|
@pytest.mark.asyncio
|
|
@@ -170,7 +170,7 @@ class TestMoonwellAdapter:
|
|
|
170
170
|
amount=10**6,
|
|
171
171
|
)
|
|
172
172
|
|
|
173
|
-
assert success
|
|
173
|
+
assert success
|
|
174
174
|
assert "simulation" in result
|
|
175
175
|
|
|
176
176
|
@pytest.mark.asyncio
|
|
@@ -198,7 +198,7 @@ class TestMoonwellAdapter:
|
|
|
198
198
|
amount=10**6,
|
|
199
199
|
)
|
|
200
200
|
|
|
201
|
-
assert success
|
|
201
|
+
assert success
|
|
202
202
|
assert "simulation" in result
|
|
203
203
|
|
|
204
204
|
@pytest.mark.asyncio
|
|
@@ -219,7 +219,7 @@ class TestMoonwellAdapter:
|
|
|
219
219
|
mtoken=MOONWELL_DEFAULTS["m_wsteth"],
|
|
220
220
|
)
|
|
221
221
|
|
|
222
|
-
assert success
|
|
222
|
+
assert success
|
|
223
223
|
assert "simulation" in result
|
|
224
224
|
|
|
225
225
|
@pytest.mark.asyncio
|
|
@@ -250,7 +250,7 @@ class TestMoonwellAdapter:
|
|
|
250
250
|
|
|
251
251
|
success, result = await adapter.claim_rewards()
|
|
252
252
|
|
|
253
|
-
assert success
|
|
253
|
+
assert success
|
|
254
254
|
assert isinstance(result, dict)
|
|
255
255
|
|
|
256
256
|
@pytest.mark.asyncio
|
|
@@ -290,7 +290,7 @@ class TestMoonwellAdapter:
|
|
|
290
290
|
|
|
291
291
|
success, result = await adapter.get_pos(mtoken=MOONWELL_DEFAULTS["m_usdc"])
|
|
292
292
|
|
|
293
|
-
assert success
|
|
293
|
+
assert success
|
|
294
294
|
assert "mtoken_balance" in result
|
|
295
295
|
assert "underlying_balance" in result
|
|
296
296
|
assert "borrow_balance" in result
|
|
@@ -316,7 +316,7 @@ class TestMoonwellAdapter:
|
|
|
316
316
|
mtoken=MOONWELL_DEFAULTS["m_wsteth"]
|
|
317
317
|
)
|
|
318
318
|
|
|
319
|
-
assert success
|
|
319
|
+
assert success
|
|
320
320
|
assert result == 0.75
|
|
321
321
|
|
|
322
322
|
@pytest.mark.asyncio
|
|
@@ -465,7 +465,7 @@ class TestMoonwellAdapter:
|
|
|
465
465
|
mtoken=MOONWELL_DEFAULTS["m_usdc"], apy_type="supply", include_rewards=False
|
|
466
466
|
)
|
|
467
467
|
|
|
468
|
-
assert success
|
|
468
|
+
assert success
|
|
469
469
|
assert isinstance(result, float)
|
|
470
470
|
assert result >= 0
|
|
471
471
|
|
|
@@ -502,7 +502,7 @@ class TestMoonwellAdapter:
|
|
|
502
502
|
mtoken=MOONWELL_DEFAULTS["m_usdc"], apy_type="borrow", include_rewards=False
|
|
503
503
|
)
|
|
504
504
|
|
|
505
|
-
assert success
|
|
505
|
+
assert success
|
|
506
506
|
assert isinstance(result, float)
|
|
507
507
|
assert result >= 0
|
|
508
508
|
|
|
@@ -523,7 +523,7 @@ class TestMoonwellAdapter:
|
|
|
523
523
|
|
|
524
524
|
success, result = await adapter.get_borrowable_amount()
|
|
525
525
|
|
|
526
|
-
assert success
|
|
526
|
+
assert success
|
|
527
527
|
assert result == 10**18
|
|
528
528
|
|
|
529
529
|
@pytest.mark.asyncio
|
|
@@ -559,7 +559,7 @@ class TestMoonwellAdapter:
|
|
|
559
559
|
|
|
560
560
|
success, result = await adapter.wrap_eth(amount=10**18)
|
|
561
561
|
|
|
562
|
-
assert success
|
|
562
|
+
assert success
|
|
563
563
|
assert "simulation" in result
|
|
564
564
|
|
|
565
565
|
def test_strategy_address_missing(self):
|
|
@@ -630,6 +630,6 @@ class TestMoonwellAdapter:
|
|
|
630
630
|
mtoken=MOONWELL_DEFAULTS["m_usdc"]
|
|
631
631
|
)
|
|
632
632
|
|
|
633
|
-
assert success
|
|
633
|
+
assert success
|
|
634
634
|
assert result["cTokens_raw"] == 0
|
|
635
635
|
assert result["underlying_raw"] == 0
|
|
@@ -44,30 +44,29 @@ else:
|
|
|
44
44
|
print(f"Error: {data}")
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
### Get
|
|
47
|
+
### Get pools (all or filtered)
|
|
48
|
+
|
|
49
|
+
Pass at least `chain_id` when fetching all pools (e.g. `chain_id=8453` for Base):
|
|
48
50
|
|
|
49
51
|
```python
|
|
50
|
-
success, data = await adapter.get_pools()
|
|
52
|
+
success, data = await adapter.get_pools(chain_id=8453)
|
|
51
53
|
if success:
|
|
52
54
|
matches = data.get("matches", [])
|
|
53
|
-
print(f"Found {len(matches)} Llama matches")
|
|
54
55
|
for match in matches:
|
|
55
56
|
if match.get("stablecoin"):
|
|
56
|
-
print(f"
|
|
57
|
+
print(f"Pool {match.get('id')} - APY: {match.get('apy')}%")
|
|
57
58
|
else:
|
|
58
59
|
print(f"Error: {data}")
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
Optional: `project="lido"` to filter by project.
|
|
62
63
|
|
|
63
64
|
## API Endpoints
|
|
64
65
|
|
|
65
|
-
The adapter uses the
|
|
66
|
+
The adapter uses the Wayfinder API:
|
|
66
67
|
|
|
67
|
-
- `GET /
|
|
68
|
-
- `
|
|
69
|
-
- `GET /api/v1/public/pools/llama/matches/` - Get Llama matches
|
|
70
|
-
- `GET /api/v1/public/pools/llama/reports/` - Get Llama reports
|
|
68
|
+
- `GET /v1/blockchain/pools/?chain_id=1&project=lido` - List pools (filter by chain_id, optional project)
|
|
69
|
+
- `POST /v1/blockchain/pools/` - Get pools by IDs (body: `{"pool_ids": ["id1", "id2"]}`)
|
|
71
70
|
|
|
72
71
|
## Error Handling
|
|
73
72
|
|
|
@@ -2,7 +2,7 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
4
4
|
from wayfinder_paths.core.clients.PoolClient import (
|
|
5
|
-
|
|
5
|
+
LlamaMatchesResponse,
|
|
6
6
|
PoolClient,
|
|
7
7
|
PoolList,
|
|
8
8
|
)
|
|
@@ -48,16 +48,15 @@ class PoolAdapter(BaseAdapter):
|
|
|
48
48
|
self.logger.error(f"Error fetching pools by IDs: {e}")
|
|
49
49
|
return (False, str(e))
|
|
50
50
|
|
|
51
|
-
async def get_pools(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"""
|
|
51
|
+
async def get_pools(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
chain_id: int | None = None,
|
|
55
|
+
project: str | None = None,
|
|
56
|
+
) -> tuple[bool, LlamaMatchesResponse | str]:
|
|
58
57
|
try:
|
|
59
|
-
data = await self.pool_client.get_pools()
|
|
58
|
+
data = await self.pool_client.get_pools(chain_id=chain_id, project=project)
|
|
60
59
|
return (True, data)
|
|
61
60
|
except Exception as e:
|
|
62
|
-
self.logger.error(f"Error fetching
|
|
61
|
+
self.logger.error(f"Error fetching pools: {e}")
|
|
63
62
|
return (False, str(e))
|
|
@@ -41,7 +41,7 @@ class TestPoolAdapter:
|
|
|
41
41
|
pool_ids=["pool-123", "pool-456"]
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
assert success
|
|
44
|
+
assert success
|
|
45
45
|
assert data == mock_response
|
|
46
46
|
mock_pool_client.get_pools_by_ids.assert_called_once_with(
|
|
47
47
|
pool_ids=["pool-123", "pool-456"]
|
|
@@ -66,7 +66,7 @@ class TestPoolAdapter:
|
|
|
66
66
|
|
|
67
67
|
success, data = await adapter.get_pools()
|
|
68
68
|
|
|
69
|
-
assert success
|
|
69
|
+
assert success
|
|
70
70
|
assert data == mock_response
|
|
71
71
|
|
|
72
72
|
@pytest.mark.asyncio
|
|
@@ -63,23 +63,11 @@ else:
|
|
|
63
63
|
print(f"Error: {data}")
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
### Get Token (Flexible)
|
|
67
|
-
|
|
68
|
-
```python
|
|
69
|
-
# Try by address first, then by token_id
|
|
70
|
-
success, data = await adapter.get_token(address="0x1234...", token_id="token-123")
|
|
71
|
-
if success:
|
|
72
|
-
print(f"Token data: {data}")
|
|
73
|
-
else:
|
|
74
|
-
print(f"Error: {data}")
|
|
75
|
-
```
|
|
76
|
-
|
|
77
66
|
## API Endpoints
|
|
78
67
|
|
|
79
|
-
The adapter uses the following
|
|
68
|
+
The adapter uses the following Wayfinder API endpoint:
|
|
80
69
|
|
|
81
|
-
|
|
82
|
-
2. **Token by ID**: `GET /public/tokens/lookup/?token_id={token_id}`
|
|
70
|
+
- `GET /api/v1/blockchain/tokens/detail/?query=...&market_data=...&chain_id=...`
|
|
83
71
|
|
|
84
72
|
## Error Handling
|
|
85
73
|
|