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.

Files changed (61) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +13 -14
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +73 -32
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
  4. wayfinder_paths/adapters/brap_adapter/README.md +11 -16
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +144 -78
  6. wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
  7. wayfinder_paths/adapters/brap_adapter/test_adapter.py +127 -65
  8. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +30 -14
  9. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +121 -67
  10. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
  11. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
  12. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
  13. wayfinder_paths/adapters/moonwell_adapter/adapter.py +332 -9
  14. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +13 -13
  15. wayfinder_paths/adapters/pool_adapter/README.md +9 -10
  16. wayfinder_paths/adapters/pool_adapter/adapter.py +9 -10
  17. wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
  18. wayfinder_paths/adapters/token_adapter/README.md +2 -14
  19. wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
  20. wayfinder_paths/adapters/token_adapter/examples.json +4 -8
  21. wayfinder_paths/adapters/token_adapter/test_adapter.py +9 -7
  22. wayfinder_paths/core/clients/BRAPClient.py +102 -61
  23. wayfinder_paths/core/clients/ClientManager.py +1 -68
  24. wayfinder_paths/core/clients/HyperlendClient.py +125 -64
  25. wayfinder_paths/core/clients/LedgerClient.py +1 -4
  26. wayfinder_paths/core/clients/PoolClient.py +122 -48
  27. wayfinder_paths/core/clients/TokenClient.py +91 -36
  28. wayfinder_paths/core/clients/WalletClient.py +26 -56
  29. wayfinder_paths/core/clients/WayfinderClient.py +28 -160
  30. wayfinder_paths/core/clients/__init__.py +0 -2
  31. wayfinder_paths/core/clients/protocols.py +35 -46
  32. wayfinder_paths/core/clients/sdk_example.py +37 -22
  33. wayfinder_paths/core/constants/erc20_abi.py +0 -11
  34. wayfinder_paths/core/engine/StrategyJob.py +10 -56
  35. wayfinder_paths/core/services/base.py +1 -0
  36. wayfinder_paths/core/services/local_evm_txn.py +25 -9
  37. wayfinder_paths/core/services/local_token_txn.py +2 -6
  38. wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
  39. wayfinder_paths/core/strategies/Strategy.py +16 -4
  40. wayfinder_paths/core/utils/evm_helpers.py +2 -9
  41. wayfinder_paths/policies/erc20.py +1 -1
  42. wayfinder_paths/run_strategy.py +13 -19
  43. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +77 -11
  44. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +6 -6
  45. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -23
  46. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
  47. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -5
  48. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2246 -1279
  49. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +276 -109
  50. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +1 -1
  51. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +153 -56
  52. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +16 -12
  53. wayfinder_paths/templates/adapter/README.md +1 -1
  54. wayfinder_paths/templates/strategy/README.md +3 -3
  55. wayfinder_paths/templates/strategy/test_strategy.py +3 -2
  56. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/METADATA +14 -49
  57. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/RECORD +59 -60
  58. wayfinder_paths/abis/generic/erc20.json +0 -383
  59. wayfinder_paths/core/clients/AuthClient.py +0 -83
  60. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/LICENSE +0 -0
  61. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/WHEEL +0 -0
@@ -217,32 +217,36 @@ async def test_canonical_usage(strategy, mock_adapter_responses):
217
217
  @pytest.mark.asyncio
218
218
  async def test_status_returns_status_dict(strategy, mock_adapter_responses):
219
219
  """Test that _status returns a proper StatusDict."""
220
+ snap = MagicMock()
221
+ snap.totals_usd = {}
222
+ snap.ltv = 0.5
223
+ snap.debt_usd = 100.0
224
+ snap.net_equity_usd = 0.0
225
+
220
226
  with patch.object(
221
- strategy, "_aggregate_positions", new_callable=AsyncMock
222
- ) as mock_pos:
223
- mock_pos.return_value = ({}, {})
224
- with patch.object(strategy, "compute_ltv", new_callable=AsyncMock) as mock_ltv:
225
- mock_ltv.return_value = 0.5
227
+ strategy, "_accounting_snapshot", new_callable=AsyncMock
228
+ ) as mock_snap:
229
+ mock_snap.return_value = (snap, (0.8, 0.8))
230
+ with patch.object(
231
+ strategy, "_get_gas_balance", new_callable=AsyncMock
232
+ ) as mock_gas:
233
+ mock_gas.return_value = 100000000000000000 # 0.1 ETH
226
234
  with patch.object(
227
- strategy, "_get_gas_balance", new_callable=AsyncMock
228
- ) as mock_gas:
229
- mock_gas.return_value = 100000000000000000 # 0.1 ETH
235
+ strategy, "get_peg_diff", new_callable=AsyncMock
236
+ ) as mock_peg:
237
+ mock_peg.return_value = 0.001
230
238
  with patch.object(
231
- strategy, "get_peg_diff", new_callable=AsyncMock
232
- ) as mock_peg:
233
- mock_peg.return_value = 0.001
234
- with patch.object(
235
- strategy, "quote", new_callable=AsyncMock
236
- ) as mock_quote:
237
- mock_quote.return_value = {"apy": 0.1, "data": {}}
239
+ strategy, "quote", new_callable=AsyncMock
240
+ ) as mock_quote:
241
+ mock_quote.return_value = {"apy": 0.1, "data": {}}
238
242
 
239
- status = await strategy._status()
243
+ status = await strategy._status()
240
244
 
241
- assert "portfolio_value" in status
242
- assert "net_deposit" in status
243
- assert "strategy_status" in status
244
- assert "gas_available" in status
245
- assert "gassed_up" in status
245
+ assert "portfolio_value" in status
246
+ assert "net_deposit" in status
247
+ assert "strategy_status" in status
248
+ assert "gas_available" in status
249
+ assert "gassed_up" in status
246
250
 
247
251
 
248
252
  @pytest.mark.asyncio
@@ -417,93 +421,181 @@ async def test_swap_with_retries_aborts_on_unknown_outcome(
417
421
 
418
422
 
419
423
  @pytest.mark.asyncio
420
- async def test_balance_weth_debt_no_action_when_balanced(
421
- strategy, mock_adapter_responses
422
- ):
423
- """Test that _balance_weth_debt does nothing when debt is balanced."""
424
- # Mock wstETH position: 2 ETH worth
425
- strategy.moonwell_adapter.get_pos = AsyncMock(
426
- side_effect=[
427
- (True, {"underlying_balance": 2 * 10**18, "borrow_balance": 0}), # wstETH
428
- (
429
- True,
430
- {"underlying_balance": 0, "borrow_balance": 1 * 10**18},
431
- ), # WETH debt
432
- ]
433
- )
434
-
435
- # Mock prices: wstETH = $2000, WETH = $2000 (so 2 wstETH > 1 WETH debt)
436
- strategy.token_adapter.get_token_price = AsyncMock(
437
- return_value=(True, {"current_price": 2000.0})
438
- )
439
-
440
- success, msg = await strategy._balance_weth_debt()
424
+ async def test_post_run_guard_no_action_when_delta_ok(strategy, mock_adapter_responses):
425
+ """Post-run guard should do nothing when delta is within tolerance."""
426
+ snap = MagicMock()
427
+ snap.wallet_wsteth = 0
428
+ snap.usdc_supplied = 0
429
+ snap.wsteth_supplied = 0
430
+ snap.weth_debt = 5 * 10**18
431
+ snap.debt_usd = 100.0
432
+ snap.hf = 1.3
433
+ snap.capacity_usd = 1000.0
434
+ snap.totals_usd = {
435
+ f"Base_{M_WSTETH}": 96.0,
436
+ f"Base_{M_USDC}": 0.0,
437
+ f"Base_{WETH}": -100.0,
438
+ }
441
439
 
442
- assert success is True
443
- assert "balanced" in msg.lower()
440
+ with patch.object(strategy, "_get_gas_balance", new_callable=AsyncMock) as mock_gas:
441
+ mock_gas.return_value = int(0.01 * 10**18)
442
+ with patch.object(
443
+ strategy, "_get_collateral_factors", new_callable=AsyncMock
444
+ ) as mock_cf:
445
+ mock_cf.return_value = (0.8, 0.8)
446
+ with patch.object(
447
+ strategy, "_accounting_snapshot", new_callable=AsyncMock
448
+ ) as mock_snap:
449
+ mock_snap.return_value = (snap, (0.8, 0.8))
450
+ with patch.object(
451
+ strategy, "_ensure_markets_for_state", new_callable=AsyncMock
452
+ ) as mock_markets:
453
+ mock_markets.return_value = (True, "ok")
454
+ with patch.object(
455
+ strategy,
456
+ "_reconcile_wallet_into_position",
457
+ new_callable=AsyncMock,
458
+ ) as mock_reconcile:
459
+ with patch.object(
460
+ strategy,
461
+ "_settle_weth_debt_to_target_usd",
462
+ new_callable=AsyncMock,
463
+ ) as mock_settle:
464
+ ok, msg = await strategy._post_run_guard(mode="operate")
465
+ assert ok is True
466
+ assert "delta ok" in msg.lower()
467
+ assert not mock_reconcile.called
468
+ assert not mock_settle.called
444
469
 
445
470
 
446
471
  @pytest.mark.asyncio
447
- async def test_balance_weth_debt_rebalances_when_excess_debt(
472
+ async def test_post_run_guard_restores_delta_via_reconcile(
448
473
  strategy, mock_adapter_responses
449
474
  ):
450
- """Test that _balance_weth_debt attempts to rebalance when debt exceeds collateral."""
451
- # Mock positions: wstETH value < WETH debt
452
- strategy.moonwell_adapter.get_pos = AsyncMock(
453
- side_effect=[
454
- (True, {"underlying_balance": 1 * 10**18}), # 1 wstETH
455
- (True, {"borrow_balance": 2 * 10**18}), # 2 WETH debt (excess)
456
- ]
457
- )
458
-
459
- # Mock prices
460
- strategy.token_adapter.get_token_price = AsyncMock(
461
- return_value=(True, {"current_price": 2000.0})
462
- )
463
-
464
- # Mock wallet balances (has WETH to repay)
465
- strategy.balance_adapter.get_balance = AsyncMock(
466
- side_effect=[
467
- (True, 1 * 10**18), # WETH balance
468
- (True, 0), # ETH balance
469
- ]
470
- )
471
-
472
- strategy.MIN_GAS = 0.005
473
-
474
- success, msg = await strategy._balance_weth_debt()
475
-
476
- # Should have attempted repayment
477
- strategy.moonwell_adapter.repay.assert_called()
475
+ """Post-run guard should attempt wallet reconcile in operate mode when net short."""
476
+ snap1 = MagicMock()
477
+ snap1.wallet_wsteth = 0
478
+ snap1.usdc_supplied = 0
479
+ snap1.wsteth_supplied = 0
480
+ snap1.weth_debt = 5 * 10**18
481
+ snap1.debt_usd = 100.0
482
+ snap1.hf = 1.3
483
+ snap1.capacity_usd = 1000.0
484
+ snap1.totals_usd = {f"Base_{M_WSTETH}": 0.0, f"Base_{WETH}": -100.0}
485
+
486
+ snap2 = MagicMock()
487
+ snap2.wallet_wsteth = 0
488
+ snap2.usdc_supplied = 0
489
+ snap2.wsteth_supplied = 0
490
+ snap2.weth_debt = 5 * 10**18
491
+ snap2.debt_usd = 100.0
492
+ snap2.hf = 1.3
493
+ snap2.capacity_usd = 1000.0
494
+ snap2.totals_usd = {f"Base_{M_WSTETH}": 98.0, f"Base_{WETH}": -100.0}
495
+
496
+ with patch.object(strategy, "_get_gas_balance", new_callable=AsyncMock) as mock_gas:
497
+ mock_gas.return_value = int(0.01 * 10**18)
498
+ with patch.object(
499
+ strategy, "_get_collateral_factors", new_callable=AsyncMock
500
+ ) as mock_cf:
501
+ mock_cf.return_value = (0.8, 0.8)
502
+ with patch.object(
503
+ strategy, "_accounting_snapshot", new_callable=AsyncMock
504
+ ) as mock_snap:
505
+ mock_snap.side_effect = [(snap1, (0.8, 0.8)), (snap2, (0.8, 0.8))]
506
+ with patch.object(
507
+ strategy, "_ensure_markets_for_state", new_callable=AsyncMock
508
+ ) as mock_markets:
509
+ mock_markets.return_value = (True, "ok")
510
+ with patch.object(
511
+ strategy,
512
+ "_reconcile_wallet_into_position",
513
+ new_callable=AsyncMock,
514
+ ) as mock_reconcile:
515
+ mock_reconcile.return_value = (True, "ok")
516
+ with patch.object(
517
+ strategy,
518
+ "_settle_weth_debt_to_target_usd",
519
+ new_callable=AsyncMock,
520
+ ) as mock_settle:
521
+ ok, msg = await strategy._post_run_guard(mode="operate")
522
+ assert ok is True
523
+ assert "restored by reconcile" in msg.lower()
524
+ assert mock_reconcile.called
525
+ assert not mock_settle.called
478
526
 
479
527
 
480
528
  @pytest.mark.asyncio
481
- async def test_balance_weth_debt_rebalances_when_no_wsteth_position(
529
+ async def test_post_run_guard_delevers_to_delta_when_reconcile_insufficient(
482
530
  strategy, mock_adapter_responses
483
531
  ):
484
- """Test that _balance_weth_debt still rebalances when wstETH position fetch fails."""
485
- strategy.moonwell_adapter.get_pos = AsyncMock(
486
- side_effect=[
487
- (False, "rpc error"), # wstETH (treat as 0)
488
- (True, {"borrow_balance": 2 * 10**18}), # WETH debt
489
- ]
490
- )
491
-
492
- strategy.token_adapter.get_token_price = AsyncMock(
493
- return_value=(True, {"current_price": 2000.0})
494
- )
495
-
496
- strategy.balance_adapter.get_balance = AsyncMock(
497
- side_effect=[
498
- (True, 2 * 10**18), # WETH balance (enough to repay)
499
- ]
500
- )
501
-
502
- success, msg = await strategy._balance_weth_debt()
503
-
504
- assert success is True
505
- assert "balanced" in msg.lower()
506
- strategy.moonwell_adapter.repay.assert_called()
532
+ """Post-run guard should delever debt when still net short after reconcile."""
533
+ snap1 = MagicMock()
534
+ snap1.wallet_wsteth = 0
535
+ snap1.usdc_supplied = 0
536
+ snap1.wsteth_supplied = 0
537
+ snap1.weth_debt = 5 * 10**18
538
+ snap1.debt_usd = 100.0
539
+ snap1.hf = 1.3
540
+ snap1.capacity_usd = 1000.0
541
+ snap1.totals_usd = {f"Base_{M_WSTETH}": 0.0, f"Base_{WETH}": -100.0}
542
+
543
+ snap2 = MagicMock()
544
+ snap2.wallet_wsteth = 0
545
+ snap2.usdc_supplied = 0
546
+ snap2.wsteth_supplied = 0
547
+ snap2.weth_debt = 5 * 10**18
548
+ snap2.debt_usd = 100.0
549
+ snap2.hf = 1.3
550
+ snap2.capacity_usd = 1000.0
551
+ snap2.totals_usd = {f"Base_{M_WSTETH}": 0.0, f"Base_{WETH}": -100.0}
552
+
553
+ snap3 = MagicMock()
554
+ snap3.wallet_wsteth = 0
555
+ snap3.usdc_supplied = 0
556
+ snap3.wsteth_supplied = 0
557
+ snap3.weth_debt = int(0.005 * 10**18) # small debt
558
+ snap3.debt_usd = 12.0
559
+ snap3.hf = 1.3
560
+ snap3.capacity_usd = 1000.0
561
+ # mismatch = debt_usd - wsteth_coll_usd = 12 - 15 = -3 (within DELTA_TOL_USD of 5)
562
+ snap3.totals_usd = {f"Base_{M_WSTETH}": 15.0, f"Base_{WETH}": -12.0}
563
+
564
+ with patch.object(strategy, "_get_gas_balance", new_callable=AsyncMock) as mock_gas:
565
+ mock_gas.return_value = int(0.01 * 10**18)
566
+ with patch.object(
567
+ strategy, "_get_collateral_factors", new_callable=AsyncMock
568
+ ) as mock_cf:
569
+ mock_cf.return_value = (0.8, 0.8)
570
+ with patch.object(
571
+ strategy, "_accounting_snapshot", new_callable=AsyncMock
572
+ ) as mock_snap:
573
+ mock_snap.side_effect = [
574
+ (snap1, (0.8, 0.8)),
575
+ (snap2, (0.8, 0.8)),
576
+ (snap3, (0.8, 0.8)),
577
+ ]
578
+ with patch.object(
579
+ strategy, "_ensure_markets_for_state", new_callable=AsyncMock
580
+ ) as mock_markets:
581
+ mock_markets.return_value = (True, "ok")
582
+ with patch.object(
583
+ strategy,
584
+ "_reconcile_wallet_into_position",
585
+ new_callable=AsyncMock,
586
+ ) as mock_reconcile:
587
+ mock_reconcile.return_value = (True, "ok")
588
+ with patch.object(
589
+ strategy,
590
+ "_settle_weth_debt_to_target_usd",
591
+ new_callable=AsyncMock,
592
+ ) as mock_settle:
593
+ mock_settle.return_value = (True, "ok")
594
+ ok, msg = await strategy._post_run_guard(mode="operate")
595
+ assert ok is True
596
+ assert "delta ok" in msg.lower()
597
+ assert mock_reconcile.called
598
+ assert mock_settle.called
507
599
 
508
600
 
509
601
  @pytest.mark.asyncio
@@ -569,17 +661,21 @@ async def test_atomic_deposit_iteration_swaps_from_eth_when_borrow_surfaces_as_e
569
661
 
570
662
 
571
663
  @pytest.mark.asyncio
572
- async def test_complete_unpaired_weth_borrow_uses_eth_inventory(
664
+ async def test_reconcile_wallet_into_position_uses_eth_inventory(
573
665
  strategy, mock_adapter_responses
574
666
  ):
575
667
  """If debt exists but wstETH collateral is missing, use wallet ETH to complete the loop."""
576
- # wstETH pos is empty; WETH debt exists
577
- strategy.moonwell_adapter.get_pos = AsyncMock(
578
- side_effect=[
579
- (True, {"underlying_balance": 0}), # mwstETH
580
- (True, {"borrow_balance": 5 * 10**18}), # mWETH debt
581
- ]
582
- )
668
+
669
+ async def get_pos_side_effect(*, mtoken: str):
670
+ if mtoken == M_USDC:
671
+ return (True, {"underlying_balance": 0, "borrow_balance": 0})
672
+ if mtoken == M_WSTETH:
673
+ return (True, {"underlying_balance": 0, "borrow_balance": 0})
674
+ if mtoken == M_WETH:
675
+ return (True, {"underlying_balance": 0, "borrow_balance": 5 * 10**18})
676
+ return (True, {})
677
+
678
+ strategy.moonwell_adapter.get_pos = AsyncMock(side_effect=get_pos_side_effect)
583
679
 
584
680
  balances: dict[str, int] = {
585
681
  ETH_TOKEN_ID: 10 * 10**18,
@@ -604,10 +700,13 @@ async def test_complete_unpaired_weth_borrow_uses_eth_inventory(
604
700
 
605
701
  strategy._swap_with_retries = AsyncMock(side_effect=swap_side_effect)
606
702
 
607
- success, msg = await strategy._complete_unpaired_weth_borrow()
703
+ success, msg = await strategy._reconcile_wallet_into_position(
704
+ collateral_factors=(0.8, 0.8),
705
+ max_batch_usd=100000.0,
706
+ )
608
707
 
609
708
  assert success is True
610
- assert "completed unpaired borrow" in msg.lower()
709
+ assert strategy._swap_with_retries.called
611
710
  strategy.moonwell_adapter.lend.assert_called()
612
711
 
613
712
 
@@ -691,6 +790,62 @@ def test_min_leverage_gain_constant_exists(strategy):
691
790
  assert strategy._MIN_LEVERAGE_GAIN_BPS == 50e-4 # 50 bps
692
791
 
693
792
 
793
+ @pytest.mark.asyncio
794
+ async def test_update_runs_post_run_guard(strategy, mock_adapter_responses):
795
+ with patch.object(strategy, "_update_impl", new_callable=AsyncMock) as mock_impl:
796
+ with patch.object(
797
+ strategy, "_post_run_guard", new_callable=AsyncMock
798
+ ) as mock_guard:
799
+ mock_impl.return_value = (True, "ok")
800
+ mock_guard.return_value = (True, "guard ok")
801
+
802
+ ok, msg = await strategy.update()
803
+
804
+ assert ok is True
805
+ assert "finalizer:" in msg
806
+ mock_guard.assert_awaited()
807
+ assert mock_guard.call_args.kwargs.get("mode") == "operate"
808
+
809
+
810
+ @pytest.mark.asyncio
811
+ async def test_update_fails_if_post_run_guard_fails(strategy, mock_adapter_responses):
812
+ with patch.object(strategy, "_update_impl", new_callable=AsyncMock) as mock_impl:
813
+ with patch.object(
814
+ strategy, "_post_run_guard", new_callable=AsyncMock
815
+ ) as mock_guard:
816
+ mock_impl.return_value = (True, "ok")
817
+ mock_guard.return_value = (False, "guard failed")
818
+
819
+ ok, msg = await strategy.update()
820
+
821
+ assert ok is False
822
+ assert "finalizer failed" in msg.lower()
823
+
824
+
825
+ @pytest.mark.asyncio
826
+ async def test_withdraw_runs_post_run_guard_only_on_failure(
827
+ strategy, mock_adapter_responses
828
+ ):
829
+ with patch.object(strategy, "_withdraw_impl", new_callable=AsyncMock) as mock_impl:
830
+ with patch.object(
831
+ strategy, "_post_run_guard", new_callable=AsyncMock
832
+ ) as mock_guard:
833
+ mock_impl.return_value = (True, "ok")
834
+ mock_guard.return_value = (True, "guard ok")
835
+
836
+ ok, _ = await strategy.withdraw()
837
+
838
+ assert ok is True
839
+ mock_guard.assert_not_awaited()
840
+
841
+ mock_impl.return_value = (False, "failed")
842
+ ok, msg = await strategy.withdraw()
843
+ assert ok is False
844
+ assert "finalizer:" in msg
845
+ mock_guard.assert_awaited()
846
+ assert mock_guard.call_args.kwargs.get("mode") == "exit"
847
+
848
+
694
849
  @pytest.mark.asyncio
695
850
  async def test_leverage_calc_handles_high_cf_w(strategy, mock_adapter_responses):
696
851
  """Test that leverage calculation handles high collateral factors safely."""
@@ -762,7 +917,13 @@ async def test_partial_liquidate_prefers_wsteth_when_excess(strategy):
762
917
  f"Base_{M_USDC}": 1000.0,
763
918
  f"Base_{WETH}": -200.0,
764
919
  }
765
- strategy._aggregate_positions = AsyncMock(return_value=({}, totals_usd))
920
+ snap = MagicMock()
921
+ snap.totals_usd = totals_usd
922
+ snap.debt_usd = 200.0
923
+ snap.wsteth_price = 2000.0
924
+ snap.wsteth_dec = 18
925
+
926
+ strategy._accounting_snapshot = AsyncMock(return_value=(snap, (0.8, 0.8)))
766
927
 
767
928
  # Collateral factors
768
929
  strategy.moonwell_adapter.get_collateral_factor = AsyncMock(
@@ -849,7 +1010,13 @@ async def test_partial_liquidate_uses_usdc_collateral_when_no_wsteth_excess(stra
849
1010
  f"Base_{M_USDC}": 500.0,
850
1011
  f"Base_{WETH}": -200.0,
851
1012
  }
852
- strategy._aggregate_positions = AsyncMock(return_value=({}, totals_usd))
1013
+ snap = MagicMock()
1014
+ snap.totals_usd = totals_usd
1015
+ snap.debt_usd = 200.0
1016
+ snap.usdc_price = 1.0
1017
+ snap.usdc_dec = 6
1018
+
1019
+ strategy._accounting_snapshot = AsyncMock(return_value=(snap, (0.8, 0.8)))
853
1020
 
854
1021
  strategy.moonwell_adapter.get_collateral_factor = AsyncMock(
855
1022
  return_value=(True, 0.8)
@@ -62,7 +62,7 @@ Transactions are scoped to the strategy wallet and Enso Router approval/swap cal
62
62
  ### Withdraw
63
63
 
64
64
  - Requires a prior deposit (the strategy tracks `self.DEPOSIT_USDC`).
65
- - Reads the pool balance via `BalanceAdapter.get_pool_balance`, unwinds via BRAP swaps back to USDC, and moves USDC from the strategy wallet to the main wallet via `BalanceAdapter.move_from_strategy_wallet_to_main_wallet`.
65
+ - Reads the pool balance via `BalanceAdapter.get_balance` (with pool address and chain_id), unwinds via BRAP swaps back to USDC, and moves USDC from the strategy wallet to the main wallet via `BalanceAdapter.move_from_strategy_wallet_to_main_wallet`.
66
66
  - Updates the ledger and clears cached pool state.
67
67
 
68
68
  ## Running locally