wayfinder-paths 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wayfinder-paths might be problematic. Click here for more details.

Files changed (115) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +394 -0
  2. wayfinder_paths/__init__.py +21 -0
  3. wayfinder_paths/config.example.json +20 -0
  4. wayfinder_paths/conftest.py +31 -0
  5. wayfinder_paths/core/__init__.py +13 -0
  6. wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
  7. wayfinder_paths/core/adapters/__init__.py +5 -0
  8. wayfinder_paths/core/adapters/base.py +5 -0
  9. wayfinder_paths/core/clients/AuthClient.py +83 -0
  10. wayfinder_paths/core/clients/BRAPClient.py +90 -0
  11. wayfinder_paths/core/clients/ClientManager.py +231 -0
  12. wayfinder_paths/core/clients/HyperlendClient.py +151 -0
  13. wayfinder_paths/core/clients/LedgerClient.py +222 -0
  14. wayfinder_paths/core/clients/PoolClient.py +96 -0
  15. wayfinder_paths/core/clients/SimulationClient.py +180 -0
  16. wayfinder_paths/core/clients/TokenClient.py +73 -0
  17. wayfinder_paths/core/clients/TransactionClient.py +47 -0
  18. wayfinder_paths/core/clients/WalletClient.py +90 -0
  19. wayfinder_paths/core/clients/WayfinderClient.py +258 -0
  20. wayfinder_paths/core/clients/__init__.py +48 -0
  21. wayfinder_paths/core/clients/protocols.py +295 -0
  22. wayfinder_paths/core/clients/sdk_example.py +115 -0
  23. wayfinder_paths/core/config.py +369 -0
  24. wayfinder_paths/core/constants/__init__.py +26 -0
  25. wayfinder_paths/core/constants/base.py +25 -0
  26. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  27. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  28. wayfinder_paths/core/engine/VaultJob.py +182 -0
  29. wayfinder_paths/core/engine/__init__.py +5 -0
  30. wayfinder_paths/core/engine/manifest.py +97 -0
  31. wayfinder_paths/core/services/__init__.py +0 -0
  32. wayfinder_paths/core/services/base.py +177 -0
  33. wayfinder_paths/core/services/local_evm_txn.py +429 -0
  34. wayfinder_paths/core/services/local_token_txn.py +231 -0
  35. wayfinder_paths/core/services/web3_service.py +45 -0
  36. wayfinder_paths/core/settings.py +61 -0
  37. wayfinder_paths/core/strategies/Strategy.py +183 -0
  38. wayfinder_paths/core/strategies/__init__.py +5 -0
  39. wayfinder_paths/core/strategies/base.py +7 -0
  40. wayfinder_paths/core/utils/__init__.py +1 -0
  41. wayfinder_paths/core/utils/evm_helpers.py +165 -0
  42. wayfinder_paths/core/utils/wallets.py +77 -0
  43. wayfinder_paths/core/wallets/README.md +91 -0
  44. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  45. wayfinder_paths/core/wallets/__init__.py +7 -0
  46. wayfinder_paths/run_strategy.py +409 -0
  47. wayfinder_paths/scripts/__init__.py +0 -0
  48. wayfinder_paths/scripts/create_strategy.py +181 -0
  49. wayfinder_paths/scripts/make_wallets.py +160 -0
  50. wayfinder_paths/scripts/validate_manifests.py +213 -0
  51. wayfinder_paths/tests/__init__.py +0 -0
  52. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  53. wayfinder_paths/tests/test_test_coverage.py +212 -0
  54. wayfinder_paths/tests/test_utils.py +64 -0
  55. wayfinder_paths/vaults/__init__.py +0 -0
  56. wayfinder_paths/vaults/adapters/__init__.py +0 -0
  57. wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
  58. wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
  59. wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
  60. wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
  61. wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
  62. wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
  63. wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
  64. wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
  65. wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
  66. wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
  67. wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
  68. wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
  69. wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
  70. wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
  71. wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
  72. wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
  73. wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
  74. wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
  75. wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
  76. wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
  77. wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
  78. wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
  79. wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
  80. wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
  81. wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
  82. wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
  83. wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
  84. wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
  85. wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
  86. wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
  87. wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
  88. wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
  89. wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
  90. wayfinder_paths/vaults/strategies/__init__.py +0 -0
  91. wayfinder_paths/vaults/strategies/config.py +85 -0
  92. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
  93. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
  94. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  95. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
  96. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
  97. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
  98. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
  99. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  100. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
  101. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
  102. wayfinder_paths/vaults/templates/adapter/README.md +105 -0
  103. wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
  104. wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
  105. wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
  106. wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
  107. wayfinder_paths/vaults/templates/strategy/README.md +152 -0
  108. wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
  109. wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
  110. wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
  111. wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
  112. wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
  113. wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
  114. wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
  115. wayfinder_paths-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,1684 @@
1
+ import asyncio
2
+ import math
3
+ import time
4
+ from datetime import UTC, datetime, timedelta
5
+ from typing import Any
6
+
7
+ from loguru import logger
8
+
9
+ from wayfinder_paths.core.constants.base import DEFAULT_SLIPPAGE
10
+ from wayfinder_paths.core.services.local_token_txn import (
11
+ LocalTokenTxnService,
12
+ )
13
+ from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
14
+ from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
15
+ from wayfinder_paths.core.wallets.WalletManager import WalletManager
16
+ from wayfinder_paths.vaults.adapters.balance_adapter.adapter import BalanceAdapter
17
+ from wayfinder_paths.vaults.adapters.brap_adapter.adapter import BRAPAdapter
18
+ from wayfinder_paths.vaults.adapters.ledger_adapter.adapter import LedgerAdapter
19
+ from wayfinder_paths.vaults.adapters.pool_adapter.adapter import PoolAdapter
20
+ from wayfinder_paths.vaults.adapters.token_adapter.adapter import TokenAdapter
21
+
22
+
23
+ class StablecoinYieldStrategy(Strategy):
24
+ name = "Stablecoin Yield Strategy"
25
+
26
+ # Strategy parameters
27
+ MIN_AMOUNT_USDC = 2
28
+ MINIMUM_DAYS_UNTIL_PROFIT = 7
29
+ MIN_TVL = 1_000_000
30
+ DUST_APY = 0.01
31
+ MIN_GAS = 10e-4 # ethereum float
32
+ SEARCH_DEPTH = 10
33
+ SUPPORTED_NETWORK_CODES = {"base"}
34
+ ROTATION_MIN_INTERVAL = timedelta(days=14)
35
+ MINIMUM_APY_IMPROVEMENT = 0.01
36
+ GAS_MAXIMUM = 0.02
37
+
38
+ description = (
39
+ "An automated yield optimization strategy that maximizes returns on USDC deposits on Base.\n\n"
40
+ "What it does: Continuously scans and evaluates yield opportunities across Base-based DeFi protocols to find the "
41
+ "highest-yielding, low-risk positions for USDC. Automatically rebalances positions when better opportunities "
42
+ "emerge to maintain optimal yield generation.\n\n"
43
+ "Exposure type: Stable USD-denominated exposure with minimal impermanent loss risk. Focuses exclusively on USDC "
44
+ "and operations on the Base network to preserve capital and maximize yield.\n\n"
45
+ "Chains: Operates solely on the Base network.\n\n"
46
+ f"Deposit/Withdrawal: Accepts deposits only in USDC on Base with a minimum of {MIN_AMOUNT_USDC} USDC. Gas: Requires Base ETH "
47
+ "for gas fees during position entry, rebalancing, and exit (~0.001-0.02 ETH per rebalance cycle). Strategy automatically "
48
+ "deploys funds to an optimal yield farming position on Base. Withdrawals exit current positions and return USDC to the "
49
+ "user wallet.\n\n"
50
+ f"Risks: Primary risks include smart contract vulnerabilities in underlying Base DeFi protocols, temporary yield fluctuations, "
51
+ f"gas costs during rebalancing, and potential brief capital lock-up during protocol transitions. Strategy filters for a minimum TVL of ${MIN_TVL:,}."
52
+ )
53
+ summary = (
54
+ "Automated stablecoin yield farming across DeFi protocols on Base. "
55
+ f"Continuously optimizes positions for maximum stable yield while avoiding impermanent loss. "
56
+ f"Min: {MIN_AMOUNT_USDC} USDC + ETH gas. Filters for ${MIN_TVL:,}+ TVL protocols."
57
+ )
58
+
59
+ # config metadata for UIs/agents
60
+ config = {
61
+ "deposit": {
62
+ "parameters": {
63
+ "main_token_amount": {
64
+ "type": "float",
65
+ "description": "amount of Base USDC (token id: usd-coin-base) to deposit",
66
+ },
67
+ "gas_token_amount": {
68
+ "type": "float",
69
+ "description": "amount of Base ETH (token id: ethereum-base) to deposit for gas fees",
70
+ "minimum": 0,
71
+ "maximum": GAS_MAXIMUM,
72
+ },
73
+ },
74
+ "process": "Deposits USDC on Base and searches for the highest yield opportunities among Base-based DeFi protocols",
75
+ "requirements": [
76
+ "Sufficient USDC balance on Base",
77
+ "Base ETH available for gas",
78
+ ],
79
+ "result": "Funds deployed to a yield farming position on Base",
80
+ },
81
+ "withdraw": {
82
+ "parameters": {},
83
+ "process": "Exits yield positions on Base and returns USDC to the user wallet",
84
+ "requirements": [
85
+ "Active positions to exit",
86
+ "Gas for transactions on Base",
87
+ ],
88
+ "result": "USDC returned to wallet and positions closed on Base",
89
+ },
90
+ "update": {
91
+ "parameters": {},
92
+ "process": "Scans for better yield opportunities on Base and rebalances positions automatically",
93
+ "frequency": "Call daily or when significant yield changes occur",
94
+ "requirements": [
95
+ "Active strategy positions on Base",
96
+ "Sufficient Base gas for rebalancing",
97
+ ],
98
+ "result": "Positions optimized for maximum yield on Base",
99
+ },
100
+ "technical_details": {
101
+ "wallet_structure": "Uses strategy subwallet for isolation",
102
+ "chains": ["Base"],
103
+ "protocols": ["Various Base DeFi yield protocols"],
104
+ "tokens": ["USDC"],
105
+ "gas_requirements": "~0.001-0.02 ETH per rebalance on Base",
106
+ "search_depth": SEARCH_DEPTH,
107
+ "minimum_tvl": MIN_TVL,
108
+ "dust_apy_threshold": DUST_APY,
109
+ "minimum_apy_edge": MINIMUM_APY_IMPROVEMENT,
110
+ "rotation_cooldown_days": ROTATION_MIN_INTERVAL.days,
111
+ "profit_horizon_days": MINIMUM_DAYS_UNTIL_PROFIT,
112
+ },
113
+ }
114
+
115
+ def __init__(
116
+ self,
117
+ config: dict[str, Any] | None = None,
118
+ *,
119
+ main_wallet: dict[str, Any] | None = None,
120
+ vault_wallet: dict[str, Any] | None = None,
121
+ simulation: bool = False,
122
+ web3_service=None,
123
+ api_key: str | None = None,
124
+ ):
125
+ super().__init__(api_key=api_key)
126
+ merged_config: dict[str, Any] = dict(config or {})
127
+ if main_wallet is not None:
128
+ merged_config["main_wallet"] = main_wallet
129
+ if vault_wallet is not None:
130
+ merged_config["vault_wallet"] = vault_wallet
131
+
132
+ self.config = merged_config
133
+ self.simulation = simulation
134
+ self.deposited_amount = 0
135
+ self.current_pool = None
136
+ self.current_apy = 0
137
+ self.balance_adapter = None
138
+ self.tx_adapter = None
139
+ self.web3_service = web3_service
140
+ self.token_adapter = None
141
+ self.ledger_adapter = None
142
+ self.pool_adapter = None
143
+ self.brap_adapter = None
144
+
145
+ try:
146
+ main_wallet_cfg = self.config.get("main_wallet")
147
+ vault_wallet_cfg = self.config.get("vault_wallet")
148
+
149
+ adapter_config = {
150
+ "main_wallet": main_wallet_cfg or None,
151
+ "vault_wallet": vault_wallet_cfg or None,
152
+ "strategy": self.config,
153
+ }
154
+
155
+ if self.web3_service is None:
156
+ wallet_provider = WalletManager.get_provider(adapter_config)
157
+ tx_adapter = LocalTokenTxnService(
158
+ adapter_config,
159
+ wallet_provider=wallet_provider,
160
+ simulation=self.simulation,
161
+ )
162
+ web3_service = DefaultWeb3Service(
163
+ wallet_provider=wallet_provider, evm_transactions=tx_adapter
164
+ )
165
+ else:
166
+ web3_service = self.web3_service
167
+ tx_adapter = web3_service.token_transactions
168
+ balance = BalanceAdapter(adapter_config, web3_service=web3_service)
169
+ token_adapter = TokenAdapter()
170
+ ledger_adapter = LedgerAdapter()
171
+ pool_adapter = PoolAdapter()
172
+ brap_adapter = BRAPAdapter(
173
+ web3_service=web3_service, simulation=self.simulation
174
+ )
175
+
176
+ self.register_adapters(
177
+ [
178
+ balance,
179
+ token_adapter,
180
+ ledger_adapter,
181
+ pool_adapter,
182
+ brap_adapter,
183
+ tx_adapter,
184
+ ]
185
+ )
186
+ self.balance_adapter = balance
187
+ self.tx_adapter = tx_adapter
188
+ self.web3_service = web3_service
189
+ self.token_adapter = token_adapter
190
+ self.ledger_adapter = ledger_adapter
191
+ self.pool_adapter = pool_adapter
192
+ self.brap_adapter = brap_adapter
193
+
194
+ except Exception:
195
+ pass
196
+
197
+ def _get_vault_wallet_address(self) -> str:
198
+ """Get vault wallet address with validation."""
199
+ vault_wallet = self.config.get("vault_wallet")
200
+ if not vault_wallet or not isinstance(vault_wallet, dict):
201
+ raise ValueError("vault_wallet not configured in strategy config")
202
+ address = vault_wallet.get("address")
203
+ if not address:
204
+ raise ValueError("vault_wallet address not found in config")
205
+ return str(address)
206
+
207
+ def _get_main_wallet_address(self) -> str:
208
+ """Get main wallet address with validation."""
209
+ main_wallet = self.config.get("main_wallet")
210
+ if not main_wallet or not isinstance(main_wallet, dict):
211
+ raise ValueError("main_wallet not configured in strategy config")
212
+ address = main_wallet.get("address")
213
+ if not address:
214
+ raise ValueError("main_wallet address not found in config")
215
+ return str(address)
216
+
217
+ async def setup(self):
218
+ logger.info("Starting StablecoinYieldStrategy setup")
219
+ start_time = time.time()
220
+
221
+ await super().setup()
222
+ self.current_combined_apy_pct = 0.0
223
+
224
+ # Get vault net deposit
225
+ try:
226
+ logger.info("Fetching vault net deposit from ledger")
227
+ vault_address = self._get_vault_wallet_address()
228
+ success, deposit_data = await self.ledger_adapter.get_vault_net_deposit(
229
+ wallet_address=vault_address,
230
+ )
231
+ if success:
232
+ self.DEPOSIT_USDC = deposit_data.get("net_deposit", 0)
233
+ logger.info(f"Vault net deposit: {self.DEPOSIT_USDC} USDC")
234
+ else:
235
+ logger.error(f"Failed to fetch vault net deposit: {deposit_data}")
236
+ self.DEPOSIT_USDC = 0
237
+ except Exception as e:
238
+ logger.error(f"Failed to fetch vault net deposit: {e}")
239
+ self.DEPOSIT_USDC = 0
240
+
241
+ # Get USDC token info
242
+ try:
243
+ logger.info("Fetching USDC token information")
244
+ success, self.usdc_token_info = await self.token_adapter.get_token(
245
+ "usd-coin-base"
246
+ )
247
+ if not success:
248
+ logger.warning("Failed to fetch USDC token info, using empty dict")
249
+ self.usdc_token_info = {}
250
+ else:
251
+ logger.info(
252
+ f"USDC token info loaded: {self.usdc_token_info.get('symbol', 'Unknown')} on {self.usdc_token_info.get('chain', {}).get('name', 'Unknown')}"
253
+ )
254
+ except Exception as e:
255
+ logger.error(f"Error fetching USDC token info: {e}")
256
+ self.usdc_token_info = {}
257
+
258
+ self.current_pool = {
259
+ "token_id": self.usdc_token_info.get("token_id"),
260
+ "name": self.usdc_token_info.get("name"),
261
+ "symbol": self.usdc_token_info.get("symbol"),
262
+ "decimals": self.usdc_token_info.get("decimals", 18),
263
+ "address": self.usdc_token_info.get("address"),
264
+ "chain": self.usdc_token_info.get("chain", {"code": "base", "id": 8453}),
265
+ }
266
+
267
+ self.current_pool_data = None
268
+
269
+ chain_code = "base" # Default to base
270
+ if self.current_pool and self.current_pool.get("chain"):
271
+ chain_code = self.current_pool.get("chain").get("code", "base")
272
+
273
+ # Get gas token info
274
+ try:
275
+ logger.info(f"Fetching gas token for chain: {chain_code}")
276
+ success, gas_token_data = await self.token_adapter.get_gas_token(chain_code)
277
+ if success:
278
+ self.gas_token = gas_token_data
279
+ logger.info(
280
+ f"Gas token loaded: {gas_token_data.get('symbol', 'Unknown')}"
281
+ )
282
+ else:
283
+ logger.warning("Failed to fetch gas token info, using empty dict")
284
+ self.gas_token = {}
285
+ except Exception as e:
286
+ logger.error(f"Error fetching gas token info: {e}")
287
+ self.gas_token = {}
288
+
289
+ if not self.DEPOSIT_USDC:
290
+ logger.info("No deposits found, setting current pool balance to 0")
291
+ self.current_pool_balance = 0
292
+ return
293
+
294
+ # Get vault transactions to determine current position
295
+ try:
296
+ logger.info("Fetching vault transaction history")
297
+ success, txns_data = await self.ledger_adapter.get_vault_transactions(
298
+ wallet_address=self._get_vault_wallet_address(),
299
+ )
300
+ if success:
301
+ txns = [
302
+ txn
303
+ for txn in txns_data.get("transactions", [])
304
+ if txn.get("operation") != "DEPOSIT"
305
+ ]
306
+ logger.info(f"Found {len(txns)} non-deposit transactions")
307
+ else:
308
+ logger.error(f"Failed to fetch vault transactions: {txns_data}")
309
+ txns = []
310
+ except Exception as e:
311
+ logger.error(f"Failed to fetch vault transactions: {e}")
312
+ txns = []
313
+
314
+ if txns and txns[-1].get("operation") != "WITHDRAW":
315
+ pos = txns[-1].get("data").get("op_data")
316
+ success, token_info = await self.token_adapter.get_token(
317
+ pos.get("to_token_id")
318
+ )
319
+ if not success:
320
+ token_info = {}
321
+ self.current_pool = {
322
+ "token_id": token_info.get("token_id"),
323
+ "name": token_info.get("name"),
324
+ "symbol": token_info.get("symbol"),
325
+ "decimals": token_info.get("decimals"),
326
+ "address": token_info.get("address"),
327
+ "chain": token_info.get("chain"),
328
+ }
329
+
330
+ success, reports = await self.pool_adapter.get_pools_by_ids(
331
+ pool_ids=[self.current_pool.get("token_id")],
332
+ merge_external=False,
333
+ )
334
+ if success and reports.get("pools"):
335
+ self.current_pool_data = reports.get("pools", [])[0]
336
+
337
+ identifiers = []
338
+ pool_id = self.current_pool.get("token_id", None)
339
+ if isinstance(pool_id, str):
340
+ identifiers.append(pool_id)
341
+
342
+ pool_address = self.current_pool.get("address", None)
343
+ pool_chain = self.current_pool.get("chain", None)
344
+ chain_code = ((pool_chain or {}).get("code")) or None
345
+ if isinstance(pool_address, str) and isinstance(chain_code, str):
346
+ identifiers.append(f"{chain_code.lower()}_{pool_address.lower()}")
347
+
348
+ llama_report = None
349
+ if identifiers:
350
+ success, llama_reports = await self.pool_adapter.get_llama_reports(
351
+ identifiers=identifiers
352
+ )
353
+ if success:
354
+ for identifier in identifiers:
355
+ if not isinstance(identifier, str):
356
+ continue
357
+ report = llama_reports.get(identifier.lower(), None)
358
+ if report:
359
+ llama_report = report
360
+ break
361
+
362
+ if self.current_pool_data is None and llama_report:
363
+ self.current_pool_data = {
364
+ **self.current_pool_data,
365
+ "llama_report": llama_report,
366
+ }
367
+
368
+ if llama_report and llama_report.get("llama_combined_apy_pct") is not None:
369
+ self.current_combined_apy_pct = (
370
+ llama_report.get("llama_combined_apy_pct", 0) / 100
371
+ )
372
+ elif llama_report and llama_report.get("llama_apy_pct") is not None:
373
+ self.current_combined_apy_pct = llama_report.get("llama_apy_pct", 0) / 100
374
+ elif self.current_pool_data:
375
+ self.current_combined_apy_pct = self.current_pool_data.get("apy", 0)
376
+
377
+ pool_address = self.current_pool.get("address")
378
+ chain_id = self.current_pool.get("chain", {}).get("id")
379
+ user_address = self._get_vault_wallet_address()
380
+ if (
381
+ pool_address
382
+ and chain_id
383
+ and user_address
384
+ and pool_address != self.usdc_token_info.get("address")
385
+ ):
386
+ try:
387
+ (
388
+ success,
389
+ current_pool_balance_raw,
390
+ ) = await self.balance_adapter.get_pool_balance(
391
+ pool_address=pool_address,
392
+ chain_id=chain_id,
393
+ user_address=user_address,
394
+ )
395
+ self.current_pool_balance = current_pool_balance_raw if success else 0
396
+ except Exception as e:
397
+ print(f"Warning: Failed to get pool balance: {e}")
398
+ self.current_pool_balance = 0
399
+ else:
400
+ self.current_pool_balance = 0
401
+
402
+ baseline_token = (
403
+ self.usdc_token_info
404
+ if self.usdc_token_info.get("chain", {}).get("id")
405
+ == self.current_pool.get("chain").get("id")
406
+ else None
407
+ )
408
+ if (
409
+ baseline_token
410
+ and self.current_pool.get("token_id") != baseline_token.get("token_id")
411
+ and self.current_pool_balance
412
+ ):
413
+ return
414
+
415
+ inferred = await self._infer_active_pool_from_wallet()
416
+ if inferred is not None:
417
+ inferred_token, inferred_balance, inferred_entry = inferred
418
+ self.current_pool = inferred_token
419
+ self.current_pool_balance = inferred_balance
420
+ if inferred_entry:
421
+ self.current_pool_data = inferred_entry
422
+ llama_combined = inferred_entry.get("llama_combined_apy_pct")
423
+ llama_apy = inferred_entry.get("llama_apy_pct")
424
+ if llama_combined is not None:
425
+ self.current_combined_apy_pct = float(llama_combined) / 100
426
+ elif llama_apy is not None:
427
+ self.current_combined_apy_pct = float(llama_apy) / 100
428
+ return
429
+
430
+ if self.usdc_token_info:
431
+ status, raw_balance = await self.balance_adapter.get_balance(
432
+ token_id=self.usdc_token_info.get("token_id"),
433
+ wallet_address=self._get_vault_wallet_address(),
434
+ )
435
+ if not status or not raw_balance:
436
+ return
437
+ try:
438
+ balance_wei = int(raw_balance)
439
+ except (TypeError, ValueError):
440
+ return
441
+ if balance_wei <= 0:
442
+ return
443
+
444
+ self.current_pool = self.usdc_token_info
445
+ self.current_pool_balance = balance_wei
446
+ self.current_combined_apy_pct = 0.0
447
+ self.current_pool_data = None
448
+
449
+ return
450
+
451
+ elapsed_time = time.time() - start_time
452
+ logger.info(
453
+ f"StablecoinYieldStrategy setup completed in {elapsed_time:.2f} seconds"
454
+ )
455
+
456
+ def _sum_non_gas_balance_usd(self, balances: list[dict[str, Any]] | None) -> float:
457
+ total_usd = 0.0
458
+ for bal in balances or []:
459
+ if self._is_gas_balance_entry(bal):
460
+ continue
461
+ usd_value = bal.get("balanceUSD")
462
+ try:
463
+ total_usd += float(usd_value or 0.0)
464
+ except (TypeError, ValueError):
465
+ continue
466
+ return total_usd
467
+
468
+ async def _infer_active_pool_from_wallet(self):
469
+ try:
470
+ (
471
+ _,
472
+ wallet_balances,
473
+ ) = await self.balance_adapter.get_all_balances(
474
+ wallet_address=self._get_vault_wallet_address(),
475
+ enrich=True,
476
+ from_cache=False,
477
+ add_llama=True,
478
+ )
479
+ except Exception:
480
+ return None
481
+
482
+ if not wallet_balances:
483
+ return None
484
+
485
+ target_chain = self.current_pool.get("chain").get("name").lower()
486
+ usdc_token = (
487
+ self.usdc_token_info
488
+ if self.usdc_token_info.get("chain").get("name").lower() == target_chain
489
+ else None
490
+ )
491
+ usdc_address = usdc_token.get("address").lower() if usdc_token else ""
492
+
493
+ best_entry = None
494
+ best_balance_wei = 0
495
+ best_balance_usd = 0.0
496
+
497
+ for balance in wallet_balances.get("balances"):
498
+ if self._is_gas_balance_entry(balance):
499
+ continue
500
+
501
+ network = (balance.get("network") or "").lower()
502
+ if network != target_chain:
503
+ continue
504
+
505
+ token_address = balance.get("tokenAddress")
506
+ if not token_address or token_address.lower() == usdc_address:
507
+ continue
508
+
509
+ raw_balance = balance.get("balance_base_units")
510
+ try:
511
+ balance_wei = int(raw_balance)
512
+ except (TypeError, ValueError):
513
+ continue
514
+ if balance_wei <= 0:
515
+ continue
516
+
517
+ balance_usd = float(balance.get("balanceUSD") or 0.0)
518
+ if balance_usd <= 0:
519
+ continue
520
+
521
+ if best_entry is None or balance_usd > best_balance_usd:
522
+ best_entry = balance
523
+ best_balance_wei = balance_wei
524
+ best_balance_usd = balance_usd
525
+
526
+ # TODO: no successful best entry found in manual testing
527
+ if not best_entry:
528
+ return None
529
+
530
+ token_id = best_entry.get("token_id")
531
+ token_address = best_entry.get("tokenAddress")
532
+ if not token_id and token_address:
533
+ token_id = f"{target_chain}_{token_address.lower()}"
534
+
535
+ if not token_id:
536
+ return None
537
+
538
+ success, token = await self.token_adapter.get_token(token_id)
539
+ if not success:
540
+ return None
541
+
542
+ try:
543
+ vault_address = self._get_vault_wallet_address()
544
+ (
545
+ success,
546
+ onchain_balance,
547
+ ) = await self.balance_adapter.get_balance(
548
+ token_id=token.get("token_id"),
549
+ wallet_address=vault_address,
550
+ )
551
+ if not success:
552
+ onchain_balance = best_balance_wei
553
+ except Exception:
554
+ onchain_balance = best_balance_wei
555
+
556
+ return token, onchain_balance, dict(best_entry)
557
+
558
+ def _is_gas_balance_entry(self, balance: dict[str, Any]) -> bool:
559
+ """Check if a balance entry represents a gas token."""
560
+ if not self.gas_token:
561
+ return False
562
+
563
+ # Check by token ID
564
+ token_id = balance.get("token_id")
565
+ if (
566
+ isinstance(token_id, str)
567
+ and token_id.lower() == self.gas_token.get("id", "").lower()
568
+ ):
569
+ return True
570
+
571
+ # Check by token address and network
572
+ token_address = balance.get("tokenAddress")
573
+ if isinstance(token_address, str):
574
+ if token_address.lower() == self.gas_token.get("address", "").lower():
575
+ return True
576
+
577
+ # Check address + network combination
578
+ network = (balance.get("network") or "").lower()
579
+ chain_code = self.current_pool.get("chain", {}).get("code", "").lower()
580
+ if (
581
+ token_address.lower() == self.gas_token.get("address", "").lower()
582
+ and network == chain_code
583
+ ):
584
+ return True
585
+
586
+ return False
587
+
588
+ async def deposit(
589
+ self, main_token_amount: float = 0.0, gas_token_amount: float = 0.0
590
+ ) -> StatusTuple:
591
+ if main_token_amount == 0.0 and gas_token_amount == 0.0:
592
+ return (
593
+ False,
594
+ "Either main_token_amount or gas_token_amount must be provided",
595
+ )
596
+
597
+ logger.info(
598
+ f"Starting deposit process for {main_token_amount} USDC and {gas_token_amount} gas"
599
+ )
600
+ start_time = time.time()
601
+
602
+ try:
603
+ token_info = self.usdc_token_info
604
+ self.current_pool = {
605
+ "token_id": token_info.get("token_id"),
606
+ "name": token_info.get("name"),
607
+ "symbol": token_info.get("symbol"),
608
+ "decimals": token_info.get("decimals"),
609
+ "address": token_info.get("address"),
610
+ "chain": token_info.get("chain"),
611
+ }
612
+ gas_token_id = self.gas_token.get("id")
613
+ logger.info(
614
+ f"Current pool set to: {token_info.get('symbol')} on {token_info.get('chain', {}).get('name')}"
615
+ )
616
+
617
+ # Check main wallet USDC balance if depositing main token
618
+ if main_token_amount > 0:
619
+ logger.info("Checking main wallet USDC balance")
620
+ (
621
+ main_usdc_status,
622
+ main_usdc_balance,
623
+ ) = await self.balance_adapter.get_balance(
624
+ token_id=token_info.get("token_id"),
625
+ wallet_address=self._get_main_wallet_address(),
626
+ )
627
+ if main_usdc_status and main_usdc_balance is not None:
628
+ try:
629
+ available_main_usdc = float(main_usdc_balance) / (
630
+ 10 ** self.current_pool.get("decimals")
631
+ )
632
+ logger.info(f"Main wallet USDC balance: {available_main_usdc}")
633
+ if available_main_usdc >= 0:
634
+ main_token_amount = min(
635
+ main_token_amount, available_main_usdc
636
+ )
637
+ logger.info(
638
+ f"Adjusted deposit amount to available balance: {main_token_amount}"
639
+ )
640
+ except Exception as e:
641
+ logger.warning(f"Error processing main wallet balance: {e}")
642
+ else:
643
+ logger.warning("Could not fetch main wallet USDC balance")
644
+
645
+ if main_token_amount < self.MIN_AMOUNT_USDC:
646
+ logger.warning(
647
+ f"Deposit amount {main_token_amount} below minimum {self.MIN_AMOUNT_USDC}"
648
+ )
649
+ return (
650
+ False,
651
+ f"Minimum deposit is {self.MIN_AMOUNT_USDC} USDC on Base. Received: {main_token_amount}",
652
+ )
653
+
654
+ # Check gas token amount if provided
655
+ if gas_token_amount > 0:
656
+ if gas_token_amount > self.GAS_MAXIMUM:
657
+ return (
658
+ False,
659
+ f"Gas token amount exceeds maximum configured gas buffer: {self.GAS_MAXIMUM}",
660
+ )
661
+
662
+ logger.info("Checking main wallet gas token balance")
663
+ gas_decimals = self.gas_token.get("decimals")
664
+ gas_symbol = self.gas_token.get("symbol")
665
+ (
666
+ _,
667
+ main_gas_raw,
668
+ ) = await self.balance_adapter.get_balance(
669
+ token_id=gas_token_id,
670
+ wallet_address=self._get_main_wallet_address(),
671
+ )
672
+ main_gas_int = (
673
+ int(main_gas_raw)
674
+ if isinstance(main_gas_raw, int)
675
+ else int(float(main_gas_raw or 0))
676
+ )
677
+ main_gas_native = float(main_gas_int) / (10**gas_decimals)
678
+
679
+ if main_gas_native < gas_token_amount:
680
+ return (
681
+ False,
682
+ f"Main wallet {gas_symbol} balance is less than the deposit amount: {main_gas_native} < {gas_token_amount}",
683
+ )
684
+
685
+ # Check gas balances for minimum requirement (only if depositing main token)
686
+ if main_token_amount > 0:
687
+ logger.info("Checking gas token balances for operations")
688
+ gas_decimals = self.gas_token.get("decimals")
689
+ gas_symbol = self.gas_token.get("symbol")
690
+ (
691
+ _,
692
+ main_gas_raw,
693
+ ) = await self.balance_adapter.get_balance(
694
+ token_id=gas_token_id,
695
+ wallet_address=self._get_main_wallet_address(),
696
+ )
697
+ (
698
+ _,
699
+ vault_gas_raw,
700
+ ) = await self.balance_adapter.get_balance(
701
+ token_id=gas_token_id,
702
+ wallet_address=self._get_vault_wallet_address(),
703
+ )
704
+ main_gas_int = (
705
+ int(main_gas_raw)
706
+ if isinstance(main_gas_raw, int)
707
+ else int(float(main_gas_raw or 0))
708
+ )
709
+ vault_gas_int = (
710
+ int(vault_gas_raw)
711
+ if isinstance(vault_gas_raw, int)
712
+ else int(float(vault_gas_raw or 0))
713
+ )
714
+ main_gas_native = float(main_gas_int) / (10**gas_decimals)
715
+ vault_gas_native = float(vault_gas_int) / (10**gas_decimals)
716
+ total_gas = main_gas_native + vault_gas_native
717
+ logger.info(
718
+ f"Gas balances - Main: {main_gas_native} {gas_symbol}, Vault: {vault_gas_native} {gas_symbol}, Total: {total_gas} {gas_symbol}"
719
+ )
720
+
721
+ # Use provided gas_token_amount if available, otherwise ensure minimum
722
+ required_gas = (
723
+ gas_token_amount if gas_token_amount > 0 else self.MIN_GAS
724
+ )
725
+ if total_gas < required_gas:
726
+ logger.warning(
727
+ f"Insufficient gas: {total_gas} < {required_gas} {gas_symbol}"
728
+ )
729
+ return (
730
+ False,
731
+ f"Need at least {required_gas} {gas_symbol} on Base for gas. You have: {total_gas}",
732
+ )
733
+
734
+ # Transfer main token if provided
735
+ if main_token_amount > 0:
736
+ self.current_pool_balance = int(
737
+ main_token_amount * (10 ** self.current_pool.get("decimals"))
738
+ )
739
+ self.DEPOSIT_USDC = main_token_amount
740
+ logger.info(f"Set deposit amount to {main_token_amount} USDC")
741
+
742
+ # Transfer USDC from main to vault wallet
743
+ logger.info("Initiating USDC transfer from main to vault wallet")
744
+ (
745
+ success,
746
+ msg,
747
+ ) = await self.balance_adapter.move_from_main_wallet_to_vault_wallet(
748
+ self.usdc_token_info.get("token_id"),
749
+ main_token_amount,
750
+ strategy_name=self.name,
751
+ )
752
+ if not success:
753
+ logger.error(f"USDC transfer failed: {msg}")
754
+ return (False, f"USDC transfer to vault failed: {msg}")
755
+ logger.info("USDC transfer completed successfully")
756
+
757
+ # Transfer gas if provided or if vault needs top-up
758
+ if gas_token_amount > 0:
759
+ # Get gas symbol if not already defined
760
+ if main_token_amount == 0:
761
+ gas_symbol = self.gas_token.get("symbol")
762
+
763
+ # Transfer the specified gas amount
764
+ logger.info(
765
+ f"Transferring {gas_token_amount} {gas_symbol} from main wallet to vault"
766
+ )
767
+ (
768
+ success,
769
+ msg,
770
+ ) = await self.balance_adapter.move_from_main_wallet_to_vault_wallet(
771
+ gas_token_id, gas_token_amount, strategy_name=self.name
772
+ )
773
+ if not success:
774
+ logger.error(f"Gas transfer failed: {msg}")
775
+ return (False, f"Gas transfer to vault failed: {msg}")
776
+ logger.info("Gas transfer completed successfully")
777
+ elif main_token_amount > 0 and vault_gas_native < self.MIN_GAS:
778
+ # Auto-top-up to minimum if no gas amount specified and depositing main token
779
+ top_up_amount = self.MIN_GAS - vault_gas_native
780
+ logger.info(
781
+ f"Vault gas insufficient, transferring {top_up_amount} {gas_symbol} from main wallet"
782
+ )
783
+ (
784
+ success,
785
+ msg,
786
+ ) = await self.balance_adapter.move_from_main_wallet_to_vault_wallet(
787
+ gas_token_id, top_up_amount, strategy_name=self.name
788
+ )
789
+ if not success:
790
+ logger.error(f"Gas transfer failed: {msg}")
791
+ return (False, f"Gas transfer to vault failed: {msg}")
792
+ logger.info("Gas transfer completed successfully")
793
+
794
+ elapsed_time = time.time() - start_time
795
+ logger.info(f"Deposit completed successfully in {elapsed_time:.2f} seconds")
796
+ return (
797
+ True,
798
+ "Deposit successful! Call update to open a position and start earning",
799
+ )
800
+ except Exception as e:
801
+ logger.error(f"Deposit process failed: {e}")
802
+ return (False, f"Deposit error: {e}")
803
+
804
+ async def withdraw(self, amount: float | None = None) -> StatusTuple:
805
+ logger.info(f"Starting withdrawal process for amount: {amount}")
806
+ start_time = time.time()
807
+
808
+ if not self.DEPOSIT_USDC:
809
+ logger.warning("No deposits found, nothing to withdraw")
810
+ return (
811
+ False,
812
+ "Nothing to withdraw from strategy, wallet should be empty already. If not, an error has happened please manually remove funds",
813
+ )
814
+ # Get current pool balance
815
+ logger.info("Fetching current pool balance")
816
+ try:
817
+ (
818
+ _,
819
+ self.current_pool_balance,
820
+ ) = await self.balance_adapter.get_pool_balance(
821
+ pool_address=self.current_pool.get("address"),
822
+ chain_id=self.current_pool.get("chain").get("id"),
823
+ user_address=self._get_vault_wallet_address(),
824
+ )
825
+ logger.info(f"Current pool balance: {self.current_pool_balance}")
826
+ except Exception as e:
827
+ logger.error(f"Failed to fetch pool balance: {e}")
828
+ self.current_pool_balance = 0
829
+
830
+ # Check if we need to swap out of current position
831
+ if (
832
+ self.current_pool.get("token_id") != self.usdc_token_info.get("token_id")
833
+ and self.current_pool_balance
834
+ ):
835
+ logger.info(
836
+ f"Need to swap from {self.current_pool.get('symbol')} to USDC before withdrawal"
837
+ )
838
+ quotes = {}
839
+ for attempt in range(4):
840
+ logger.info(
841
+ f"Getting swap quote (attempt {attempt + 1}/4) with slippage: {DEFAULT_SLIPPAGE * (attempt + 1)}"
842
+ )
843
+ try:
844
+ success, quotes = await self.brap_adapter.get_swap_quote(
845
+ from_token_address=self.current_pool.get("address"),
846
+ to_token_address=self.usdc_token_info.get("address"),
847
+ from_chain_id=self.current_pool.get("chain").get("id"),
848
+ to_chain_id=self.usdc_token_info.get("chain").get("id"),
849
+ from_address=self._get_vault_wallet_address(),
850
+ to_address=self._get_vault_wallet_address(),
851
+ amount=str(self.current_pool_balance),
852
+ slippage=DEFAULT_SLIPPAGE * (attempt + 1),
853
+ )
854
+ if (
855
+ success
856
+ and quotes.get("quotes")
857
+ and quotes.get("quotes").get("best_quote")
858
+ ):
859
+ logger.info("Successfully obtained swap quote")
860
+ break
861
+ except Exception as e:
862
+ logger.warning(f"Quote attempt {attempt + 1} failed: {e}")
863
+ if attempt == 3: # Last attempt
864
+ logger.error("All quote attempts failed")
865
+
866
+ best_quote = quotes.get("quotes").get("best_quote")
867
+ if not best_quote:
868
+ return (
869
+ False,
870
+ "Could not swap tokens out due to market conditions (balances too small to move or slippage required is too high) please manually move funds out",
871
+ )
872
+
873
+ if not best_quote.get("output_amount") or not best_quote.get(
874
+ "input_amount"
875
+ ):
876
+ return (False, "Swap quote missing required fields")
877
+
878
+ if not best_quote.get("from_amount_usd"):
879
+ input_amount = int(best_quote.get("input_amount"))
880
+ if self.current_pool.get("token_id") == self.usdc_token_info.get(
881
+ "token_id"
882
+ ):
883
+ best_quote["from_amount_usd"] = float(input_amount) / (
884
+ 10 ** self.current_pool.get("decimals")
885
+ )
886
+ else:
887
+ best_quote["from_amount_usd"] = await self._get_pool_usd_value(
888
+ self.current_pool, input_amount
889
+ )
890
+
891
+ if not best_quote.get("to_amount_usd"):
892
+ output_amount = int(best_quote.get("output_amount"))
893
+ best_quote["to_amount_usd"] = float(
894
+ output_amount
895
+ ) / 10 ** self.usdc_token_info.get("decimals")
896
+
897
+ if not self.brap_adapter:
898
+ return (
899
+ False,
900
+ "BRAP adapter not initialized; cannot unwind position.",
901
+ )
902
+
903
+ success, swap_result = await self.brap_adapter.swap_from_quote(
904
+ self.current_pool,
905
+ self.usdc_token_info,
906
+ self._get_vault_wallet_address(),
907
+ best_quote,
908
+ strategy_name=self.name,
909
+ )
910
+ if not success:
911
+ return (
912
+ False,
913
+ f"Failed to unwind position via swap: {swap_result}",
914
+ )
915
+
916
+ await self._sweep_wallet(self.usdc_token_info)
917
+ withdrawn_breakdown = []
918
+ withdrawn_token_ids = set()
919
+
920
+ if self.usdc_token_info.get("token_id") in withdrawn_token_ids:
921
+ pass
922
+ status, raw_balance = await self.balance_adapter.get_balance(
923
+ token_id=self.usdc_token_info.get("token_id"),
924
+ wallet_address=self._get_vault_wallet_address(),
925
+ )
926
+ if not status or not raw_balance:
927
+ pass
928
+ amount = float(raw_balance) / 10 ** self.usdc_token_info.get("decimals")
929
+ if amount > 0:
930
+ (
931
+ move_status,
932
+ move_message,
933
+ ) = await self.balance_adapter.move_from_vault_wallet_to_main_wallet(
934
+ self.usdc_token_info.get("token_id"),
935
+ amount,
936
+ strategy_name=self.name,
937
+ )
938
+ if not move_status:
939
+ return (False, f"USDC return to main failed: {move_message}")
940
+
941
+ withdrawn_breakdown.append(
942
+ (
943
+ self.usdc_token_info.get("symbol"),
944
+ self.usdc_token_info.get("chain").get("name"),
945
+ float(amount),
946
+ )
947
+ )
948
+ withdrawn_token_ids.add(self.usdc_token_info.get("token_id"))
949
+
950
+ if self.gas_token and self.gas_token.get("id") not in withdrawn_token_ids:
951
+ status, raw_gas = await self.balance_adapter.get_balance(
952
+ token_id=self.gas_token.get("id"),
953
+ wallet_address=self._get_vault_wallet_address(),
954
+ )
955
+ if status and raw_gas:
956
+ gas_amount = (
957
+ float(raw_gas) / 10 ** self.gas_token.get("decimals")
958
+ ) * 0.9
959
+ if gas_amount > 0:
960
+ (
961
+ move_gas_status,
962
+ move_gas_message,
963
+ ) = await self.balance_adapter.move_from_vault_wallet_to_main_wallet(
964
+ self.gas_token.get("id"),
965
+ gas_amount,
966
+ strategy_name=self.name,
967
+ )
968
+ if move_gas_status:
969
+ withdrawn_breakdown.append(
970
+ (
971
+ self.gas_token.get("symbol"),
972
+ self.gas_token.get("chain").get("name"),
973
+ float(gas_amount),
974
+ )
975
+ )
976
+ withdrawn_token_ids.add(self.gas_token.get("id"))
977
+
978
+ self.DEPOSIT_USDC = 0
979
+ self.current_pool_balance = 0
980
+
981
+ if not withdrawn_breakdown:
982
+ return (True, f"Successfully withdrew {amount} USDC from vault")
983
+
984
+ breakdown_msg = ", ".join(
985
+ f"{amount:.6f} {symbol} on {chain.capitalize()}"
986
+ for symbol, chain, amount in withdrawn_breakdown
987
+ )
988
+
989
+ elapsed_time = time.time() - start_time
990
+ logger.info(f"Withdrawal completed successfully in {elapsed_time:.2f} seconds")
991
+ return (
992
+ True,
993
+ f"Successfully withdrew {amount} USDC from vault: {breakdown_msg}",
994
+ )
995
+
996
+ async def _get_last_rotation_time(self, wallet_address: str) -> datetime | None:
997
+ success, data = await self.ledger_adapter.get_vault_latest_transactions(
998
+ wallet_address=self._get_vault_wallet_address(),
999
+ )
1000
+ if success is False:
1001
+ return None
1002
+ for transaction in data.get("transactions", []):
1003
+ op_data = transaction.get("op_data", {})
1004
+ if op_data.get("type") == "SWAP" and op_data.get(
1005
+ "to_token_id"
1006
+ ).lower() not in [
1007
+ "usd-coin-base",
1008
+ "base_0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1009
+ ]:
1010
+ created_str = transaction.get("created")
1011
+ if not created_str:
1012
+ continue
1013
+ try:
1014
+ dt = datetime.fromisoformat(created_str.replace("Z", "+00:00"))
1015
+ if dt.tzinfo is None:
1016
+ dt = dt.replace(tzinfo=UTC)
1017
+ return dt
1018
+ except (ValueError, AttributeError):
1019
+ continue
1020
+ return None
1021
+
1022
+ async def update(self):
1023
+ logger.info("Starting strategy update process")
1024
+ start_time = time.time()
1025
+
1026
+ if not self.DEPOSIT_USDC:
1027
+ logger.warning("No deposits found, cannot update strategy")
1028
+ return [False, "Nothing has been deposited in this vault, cannot update"]
1029
+
1030
+ logger.info("Getting non-gas balances")
1031
+ non_gas_balances = await self._get_non_gas_balances()
1032
+ current_target = self.current_pool
1033
+ if current_target is None:
1034
+ current_target = self.usdc_token_info
1035
+ logger.info("No current pool set, using USDC as target")
1036
+
1037
+ logger.info("Searching for best yield opportunities")
1038
+ should_deposit, pool_data = await self._find_best_pool()
1039
+ if not should_deposit:
1040
+ if (
1041
+ current_target
1042
+ and isinstance(current_target, dict)
1043
+ and await self._has_idle_assets(non_gas_balances, current_target)
1044
+ ):
1045
+ await self._sweep_wallet(current_target)
1046
+ await self._refresh_current_pool_balance()
1047
+ return (
1048
+ True,
1049
+ f"Consolidated assets into existing position {current_target.get('id')}",
1050
+ )
1051
+
1052
+ if isinstance(pool_data, dict):
1053
+ message = pool_data.get(
1054
+ "message", "No profitable pools found, staying in current pool"
1055
+ )
1056
+ else:
1057
+ message = (
1058
+ str(pool_data)
1059
+ if pool_data
1060
+ else "No profitable pools found, staying in current pool"
1061
+ )
1062
+ return False, message
1063
+
1064
+ if not isinstance(pool_data, dict):
1065
+ return [False, f"Invalid pool data format: {type(pool_data).__name__}"]
1066
+
1067
+ target_pool = pool_data.get("target_pool")
1068
+ target_pool_data = pool_data.get("target_pool_data")
1069
+ brap_quote = pool_data.get("brap_quote")
1070
+
1071
+ if not target_pool or not target_pool_data or not brap_quote:
1072
+ return [False, "Missing required pool data for rebalancing"]
1073
+
1074
+ gas_status, gas_message = await self._rebalance_gas(target_pool)
1075
+ if not gas_status:
1076
+ return [False, gas_message]
1077
+
1078
+ previous_pool = self.current_pool
1079
+
1080
+ last_rotation = await self._get_last_rotation_time(
1081
+ wallet_address=self._get_vault_wallet_address(),
1082
+ )
1083
+ if (
1084
+ previous_pool
1085
+ and isinstance(previous_pool, dict)
1086
+ and previous_pool.get("token_id") != self.usdc_token_info.get("token_id")
1087
+ and last_rotation is not None
1088
+ ):
1089
+ now = datetime.now(UTC)
1090
+ if (now - last_rotation) < self.ROTATION_MIN_INTERVAL:
1091
+ elapsed = now - last_rotation
1092
+ remaining = self.ROTATION_MIN_INTERVAL - elapsed
1093
+ remaining_days_cooldown = max(0.0, remaining.total_seconds() / 86400)
1094
+ cooldown_notice = (
1095
+ "Within 7-day cooldown; existing {coin} position retained. "
1096
+ "≈{days:.1f} days until rotation window reopens."
1097
+ ).format(
1098
+ coin=(
1099
+ previous_pool.get("token_id", "unknown")
1100
+ if isinstance(previous_pool, dict)
1101
+ else "unknown"
1102
+ ),
1103
+ days=remaining_days_cooldown,
1104
+ )
1105
+ return (True, cooldown_notice, False)
1106
+
1107
+ await self.brap_adapter.swap_from_quote(
1108
+ previous_pool,
1109
+ target_pool,
1110
+ self._get_vault_wallet_address(),
1111
+ brap_quote,
1112
+ strategy_name=self.name,
1113
+ )
1114
+
1115
+ self.current_pool = target_pool
1116
+ if self.current_pool and self.current_pool.get("token_id"):
1117
+ success, pool_reports = await self.pool_adapter.get_pools_by_ids(
1118
+ pool_ids=[self.current_pool.get("token_id")]
1119
+ )
1120
+ if success and pool_reports.get("pools"):
1121
+ self.current_pool_data = pool_reports.get("pools", [])[0]
1122
+ else:
1123
+ self.current_pool_data = None
1124
+ else:
1125
+ self.current_pool_data = None
1126
+ if self.current_pool_data:
1127
+ self.current_combined_apy_pct = self.current_pool_data.get("apy", 0)
1128
+ else:
1129
+ self.current_combined_apy_pct = (
1130
+ target_pool_data.get("llama_combined_apy_pct", 0) / 100
1131
+ if target_pool_data
1132
+ else 0
1133
+ )
1134
+ output_amount = (
1135
+ brap_quote.get("output_amount")
1136
+ if brap_quote and isinstance(brap_quote, dict)
1137
+ else None
1138
+ )
1139
+ self.current_pool_balance = (
1140
+ int(output_amount) if output_amount is not None else 0
1141
+ )
1142
+
1143
+ await asyncio.sleep(2)
1144
+ await self._sweep_wallet(target_pool)
1145
+ await self._refresh_current_pool_balance()
1146
+
1147
+ elapsed_time = time.time() - start_time
1148
+ logger.info(
1149
+ f"Strategy update completed successfully in {elapsed_time:.2f} seconds"
1150
+ )
1151
+ return [True, "Updated successfully"]
1152
+
1153
+ async def _refresh_current_pool_balance(self):
1154
+ pool = self.current_pool
1155
+ if not pool or pool.get("chain") is None:
1156
+ return
1157
+
1158
+ vault_address = self._get_vault_wallet_address()
1159
+ try:
1160
+ (
1161
+ _,
1162
+ refreshed_pool_balance,
1163
+ ) = await self.balance_adapter.get_pool_balance(
1164
+ pool_address=pool.get("address"),
1165
+ chain_id=pool.get("chain").get("id"),
1166
+ user_address=vault_address,
1167
+ )
1168
+ self.current_pool_balance = int(refreshed_pool_balance)
1169
+ except Exception:
1170
+ pass
1171
+
1172
+ # TODO untested
1173
+ async def _sweep_wallet(self, target_token):
1174
+ (
1175
+ _,
1176
+ wallet_balances,
1177
+ ) = await self.balance_adapter.get_all_balances(
1178
+ wallet_address=self._get_vault_wallet_address(),
1179
+ enrich=True,
1180
+ from_cache=False,
1181
+ add_llama=True,
1182
+ )
1183
+ target_chain = target_token.get("chain").get("name").lower()
1184
+ target_address = target_token.get("address").lower()
1185
+
1186
+ for balance in wallet_balances.get("balances"):
1187
+ if self._is_gas_balance_entry(balance):
1188
+ continue
1189
+
1190
+ balance_raw = balance.get("balance_base_units")
1191
+ token_address = balance.get("tokenAddress")
1192
+ network = balance.get("network", "").lower()
1193
+
1194
+ if not balance_raw or not token_address:
1195
+ continue
1196
+ if network == target_chain and token_address.lower() == target_address:
1197
+ continue
1198
+
1199
+ try:
1200
+ token_1_id = f"{network}_{token_address.lower()}"
1201
+ target_token_id = f"{target_chain}_{target_address.lower()}"
1202
+
1203
+ success, msg = await self.brap_adapter.swap_from_token_ids(
1204
+ token_1_id,
1205
+ target_token_id,
1206
+ self._get_vault_wallet_address(),
1207
+ balance_raw,
1208
+ strategy_name=self.name,
1209
+ )
1210
+ if not success:
1211
+ return (False, f"Swap failed: {msg}")
1212
+ except Exception:
1213
+ continue
1214
+
1215
+ async def _rebalance_gas(self, target_pool) -> tuple[bool, str]:
1216
+ if self.gas_token.get("chain").get("id") != target_pool.get("chain").get("id"):
1217
+ return False, "Unsupported chain for gas management."
1218
+
1219
+ # TODO: do we need to categorize vault wallet addresses?
1220
+ vault_address = self._get_vault_wallet_address()
1221
+
1222
+ required_gas = int(self.MIN_GAS * (10 ** self.gas_token.get("decimals")))
1223
+ _, current_gas = await self.balance_adapter.get_balance(
1224
+ token_id=self.gas_token.get("id"),
1225
+ wallet_address=vault_address,
1226
+ )
1227
+ if current_gas >= required_gas:
1228
+ return True, "Enough gas balance found."
1229
+
1230
+ current_native = float(current_gas) / 10 ** self.gas_token.get("decimals")
1231
+ shortfall = max(self.MIN_GAS - current_native, 0)
1232
+
1233
+ return (
1234
+ False,
1235
+ f"Vault wallet does not have enough gas. Shortfall: {shortfall} {self.gas_token.get('symbol')}",
1236
+ )
1237
+
1238
+ async def _has_idle_assets(self, balances, target_token) -> bool:
1239
+ for balance in balances:
1240
+ if self._balance_matches_token(balance, target_token):
1241
+ continue
1242
+ amount = balance.get("_amount_wei")
1243
+ if isinstance(amount, int) and amount > 0:
1244
+ return True
1245
+ return False
1246
+
1247
+ def _balance_matches_token(self, balance, token) -> bool:
1248
+ token_id = balance.get("token_id")
1249
+ if (
1250
+ isinstance(token_id, str)
1251
+ and token_id.lower() == token.get("token_id").lower()
1252
+ ):
1253
+ return True
1254
+
1255
+ token_address = balance.get("tokenAddress")
1256
+ if not isinstance(token_address, str):
1257
+ return False
1258
+
1259
+ network = (balance.get("network") or "").lower()
1260
+ chain_names = {
1261
+ getattr(token.get("chain"), "name", "").lower(),
1262
+ getattr(token.get("chain"), "code", "").lower(),
1263
+ }
1264
+
1265
+ return network in chain_names and token_address.lower() == token.address.lower()
1266
+
1267
+ async def _get_pool_usd_value(self, token, amount):
1268
+ chain_id = token.get("chain").get("id")
1269
+ if chain_id != self.usdc_token_info.get("chain").get("id"):
1270
+ return 0.0
1271
+
1272
+ success, exit_quotes = await self.brap_adapter.get_swap_quote(
1273
+ from_token_address=token.get("address"),
1274
+ to_token_address=self.usdc_token_info.get("address"),
1275
+ from_chain_id=chain_id,
1276
+ to_chain_id=self.usdc_token_info.get("chain").get("id"),
1277
+ from_address=self._get_vault_wallet_address(),
1278
+ to_address=self._get_vault_wallet_address(),
1279
+ amount=str(amount),
1280
+ )
1281
+ if not success:
1282
+ return 0.0
1283
+
1284
+ best_quote = exit_quotes.get("quotes").get("best_quote")
1285
+ current_pool_usd_value = best_quote.get("output_amount")
1286
+
1287
+ return float(
1288
+ float(current_pool_usd_value) / (10 ** self.usdc_token_info.get("decimals"))
1289
+ )
1290
+
1291
+ async def _get_non_gas_balances(self) -> list[dict[str, Any]]:
1292
+ results = []
1293
+
1294
+ if self.current_pool != self.usdc_token_info:
1295
+ (
1296
+ status,
1297
+ balance_wei,
1298
+ ) = await self.balance_adapter.get_balance(
1299
+ token_id=self.usdc_token_info.get("token_id"),
1300
+ wallet_address=self._get_vault_wallet_address(),
1301
+ )
1302
+ if not status or not balance_wei:
1303
+ pass
1304
+ results.append(
1305
+ {
1306
+ "token_id": self.usdc_token_info.get("token_id"),
1307
+ "tokenAddress": self.usdc_token_info.get("address"),
1308
+ "network": self.usdc_token_info.get("chain").get("code").upper(),
1309
+ "_amount_wei": balance_wei,
1310
+ }
1311
+ )
1312
+ else:
1313
+ results.append(
1314
+ {
1315
+ "token_id": self.usdc_token_info.get("token_id"),
1316
+ "tokenAddress": self.usdc_token_info.get("address"),
1317
+ "network": self.usdc_token_info.get("chain").get("code").upper(),
1318
+ "_amount_wei": self.current_pool_balance
1319
+ * (10 ** self.usdc_token_info.get("decimals")),
1320
+ }
1321
+ )
1322
+
1323
+ try:
1324
+ (
1325
+ _,
1326
+ wallet_balances,
1327
+ ) = await self.balance_adapter.get_all_balances(
1328
+ wallet_address=self._get_vault_wallet_address(),
1329
+ enrich=True,
1330
+ from_cache=False,
1331
+ add_llama=True,
1332
+ )
1333
+ except Exception:
1334
+ wallet_balances = []
1335
+
1336
+ for balance in wallet_balances.get("balances"):
1337
+ if self._is_gas_balance_entry(balance):
1338
+ continue
1339
+
1340
+ raw = balance.get("balance_base_units")
1341
+ try:
1342
+ amount_wei = int(raw)
1343
+ except (TypeError, ValueError):
1344
+ continue
1345
+ if amount_wei <= 0:
1346
+ continue
1347
+
1348
+ token_address = balance.get("tokenAddress")
1349
+ network = (balance.get("network") or "").lower()
1350
+ if not token_address or not network:
1351
+ continue
1352
+
1353
+ candidate = {
1354
+ "token_id": balance.get("token_id")
1355
+ or f"{network}_{token_address.lower()}",
1356
+ "tokenAddress": token_address,
1357
+ "network": network.upper(),
1358
+ "_amount_wei": amount_wei,
1359
+ }
1360
+
1361
+ exists = any(
1362
+ entry.get("token_id").lower() == candidate.get("token_id").lower()
1363
+ for entry in results
1364
+ )
1365
+ if not exists:
1366
+ results.append(candidate)
1367
+
1368
+ return results
1369
+
1370
+ async def _find_best_pool(self) -> tuple[bool, dict[str, Any]]:
1371
+ success, llama_data = await self.pool_adapter.get_llama_matches()
1372
+ if not success:
1373
+ return False, {"message": f"Failed to fetch Llama data: {llama_data}"}
1374
+
1375
+ llama_pools = [
1376
+ pool
1377
+ for pool in llama_data.get("matches", [])
1378
+ if pool.get("llama_stablecoin")
1379
+ and pool.get("llama_il_risk") == "no"
1380
+ and pool.get("llama_tvl_usd") > self.MIN_TVL
1381
+ and pool.get("llama_apy_pct") > self.DUST_APY
1382
+ and pool.get("network", "").lower() in self.SUPPORTED_NETWORK_CODES
1383
+ ]
1384
+ llama_pools = sorted(
1385
+ llama_pools, key=lambda pool: pool.get("llama_apy_pct"), reverse=True
1386
+ )
1387
+ if not llama_pools:
1388
+ return False, {"message": "No suitable pools found."}
1389
+
1390
+ for candidate in llama_pools[: self.SEARCH_DEPTH]:
1391
+ if candidate.get("address") == self.current_pool.get("address"):
1392
+ return False, {"message": "Already in the best pool, no action needed."}
1393
+
1394
+ try:
1395
+ target_status, target_pool = await self.token_adapter.get_token(
1396
+ address=candidate.get("address")
1397
+ )
1398
+ if not target_status and candidate.get("token_id"):
1399
+ target_status, target_pool = await self.token_adapter.get_token(
1400
+ token_id=candidate.get("token_id")
1401
+ )
1402
+ if not target_status and candidate.get("pool_id"):
1403
+ target_status, target_pool = await self.token_adapter.get_token(
1404
+ token_id=candidate.get("pool_id")
1405
+ )
1406
+ if not target_status:
1407
+ continue
1408
+ except Exception:
1409
+ continue
1410
+
1411
+ brap_quote = await self._search(
1412
+ candidate,
1413
+ self.current_pool,
1414
+ target_pool,
1415
+ self.current_combined_apy_pct,
1416
+ int(
1417
+ self.current_pool_balance
1418
+ * (10 ** self.current_pool.get("decimals"))
1419
+ ),
1420
+ )
1421
+ if brap_quote:
1422
+ return True, {
1423
+ "target_pool": target_pool,
1424
+ "target_pool_data": candidate,
1425
+ "brap_quote": brap_quote,
1426
+ }
1427
+
1428
+ return False, {"message": "No suitable pools found after searching."}
1429
+
1430
+ async def _search(
1431
+ self,
1432
+ pool_data,
1433
+ current_token,
1434
+ token,
1435
+ current_combined_apy_pct,
1436
+ current_token_balance,
1437
+ ):
1438
+ if token is None or current_token is None:
1439
+ return None
1440
+ if token is None or token.get("chain") is None:
1441
+ return None
1442
+ if current_token is None or current_token.get("chain") is None:
1443
+ return None
1444
+
1445
+ try:
1446
+ combined_apy_pct = pool_data.get("llama_combined_apy_pct") / 100
1447
+ success, quotes = await self.brap_adapter.get_swap_quote(
1448
+ from_token_address=current_token.get("address"),
1449
+ to_token_address=token.get("address"),
1450
+ from_chain_id=current_token.get("chain").get("id"),
1451
+ to_chain_id=token.get("chain").get("id"),
1452
+ from_address=self._get_vault_wallet_address(),
1453
+ to_address=self._get_vault_wallet_address(),
1454
+ amount=str(current_token_balance),
1455
+ )
1456
+ if not success:
1457
+ return None
1458
+ quotes_data = quotes.get("quotes") if isinstance(quotes, dict) else None
1459
+ if not isinstance(quotes_data, dict):
1460
+ return None
1461
+ best_quote = quotes_data.get("best_quote")
1462
+ if not best_quote:
1463
+ return None
1464
+
1465
+ target_pool_usd_val = await self._get_pool_usd_value(
1466
+ token, best_quote.get("output_amount")
1467
+ )
1468
+
1469
+ if current_token.get("token_id") != self.usdc_token_info.get("token_id"):
1470
+ current_pool_usd_val = await self._get_pool_usd_value(
1471
+ current_token, best_quote.get("input_amount")
1472
+ )
1473
+ else:
1474
+ current_pool_usd_val = float(
1475
+ float(self.current_pool_balance)
1476
+ / (10 ** current_token.get("decimals"))
1477
+ )
1478
+
1479
+ gas_cost = await self._get_gas_value(best_quote.get("input_amount"))
1480
+ fee_cost = (current_pool_usd_val - target_pool_usd_val) + gas_cost
1481
+ delta_combined_apy_pct = combined_apy_pct - current_combined_apy_pct
1482
+
1483
+ if delta_combined_apy_pct < self.MINIMUM_APY_IMPROVEMENT:
1484
+ return None
1485
+
1486
+ estimated_profit = (
1487
+ self.MINIMUM_DAYS_UNTIL_PROFIT
1488
+ * ((delta_combined_apy_pct * current_pool_usd_val) / 365)
1489
+ - fee_cost
1490
+ )
1491
+
1492
+ if estimated_profit > 0:
1493
+ best_quote["from_amount_usd"] = current_pool_usd_val
1494
+ best_quote["to_amount_usd"] = target_pool_usd_val
1495
+ return best_quote
1496
+
1497
+ except Exception:
1498
+ return {}
1499
+
1500
+ async def _get_gas_value(self, amount):
1501
+ token = self.gas_token
1502
+ success, gas_price_data = await self.token_adapter.get_token_price(
1503
+ token.get("token_id")
1504
+ )
1505
+ if not success:
1506
+ return 0.0
1507
+ gas_price = gas_price_data.get("current_price", 0.0)
1508
+ return float(gas_price) * float(amount) / (10 ** token.get("decimals"))
1509
+
1510
+ async def _status(self) -> StatusDict:
1511
+ if not self.DEPOSIT_USDC:
1512
+ (
1513
+ _,
1514
+ wallet_balances,
1515
+ ) = await self.balance_adapter.get_all_balances(
1516
+ wallet_address=self._get_main_wallet_address(),
1517
+ enrich=True,
1518
+ from_cache=False,
1519
+ add_llama=True,
1520
+ )
1521
+ total_value = self._sum_non_gas_balance_usd(wallet_balances.get("balances"))
1522
+ status_payload = {
1523
+ "info": "No recorded vault deposits; reporting current wallet holdings only.",
1524
+ "idle_usd": total_value,
1525
+ }
1526
+
1527
+ return StatusDict(
1528
+ portfolio_value=total_value,
1529
+ net_deposit=0,
1530
+ strategy_status=status_payload,
1531
+ )
1532
+ (
1533
+ _,
1534
+ wallet_balances,
1535
+ ) = await self.balance_adapter.get_all_balances(
1536
+ wallet_address=self._get_vault_wallet_address(),
1537
+ enrich=True,
1538
+ from_cache=False,
1539
+ add_llama=True,
1540
+ )
1541
+ total_value = self._sum_non_gas_balance_usd(wallet_balances.get("balances"))
1542
+ status_payload = (
1543
+ {
1544
+ "current_pool": self.current_pool.get("token_id"),
1545
+ "carrying_loss": None,
1546
+ "pool_balance": self.current_pool_balance
1547
+ / (10 ** self.current_pool.get("decimals")),
1548
+ "pool_apy": f"{self.current_combined_apy_pct * 100}%",
1549
+ "pool_tvl": (
1550
+ self.current_pool_data.get("tvl")
1551
+ if self.current_pool_data
1552
+ else None
1553
+ ),
1554
+ }
1555
+ if self.current_pool
1556
+ else {}
1557
+ )
1558
+
1559
+ return StatusDict(
1560
+ portfolio_value=total_value,
1561
+ net_deposit=self.DEPOSIT_USDC,
1562
+ strategy_status=status_payload,
1563
+ )
1564
+
1565
+ @staticmethod
1566
+ def policies() -> list[str]:
1567
+ enso_router = "0xF75584eF6673aD213a685a1B58Cc0330B8eA22Cf".lower()
1568
+ approve_enso = (
1569
+ "eth.tx.data[0..10] == '0x095ea7b3' && "
1570
+ f"eth.tx.data[34..74] == '{enso_router[2:]}'"
1571
+ )
1572
+ swap_enso = f"eth.tx.to == '{enso_router}'"
1573
+ wallet_id = "wallet.id == 'FORMAT_WALLET_ID'"
1574
+ return [f"({wallet_id}) && (({approve_enso}) || ({swap_enso})) "]
1575
+
1576
+ async def partial_liquidate(self, usd_value: float) -> StatusTuple:
1577
+ (
1578
+ _,
1579
+ wallet_balances,
1580
+ ) = await self.balance_adapter.get_all_balances(
1581
+ wallet_address=self._get_vault_wallet_address(),
1582
+ enrich=True,
1583
+ from_cache=False,
1584
+ add_llama=True,
1585
+ )
1586
+ _, total_usdc_wei = await self.balance_adapter.get_balance(
1587
+ token_id=self.usdc_token_info.get("token_id"),
1588
+ wallet_address=self._get_vault_wallet_address(),
1589
+ )
1590
+ available_usdc_usd = float(total_usdc_wei or 0) / (
1591
+ 10 ** self.usdc_token_info.get("decimals")
1592
+ )
1593
+
1594
+ for balance in wallet_balances.get("balances"):
1595
+ if available_usdc_usd >= usd_value:
1596
+ break
1597
+
1598
+ token_id = balance.get("token_id")
1599
+ if token_id == self.usdc_token_info.get("token_id"):
1600
+ continue
1601
+ if token_id == self.gas_token.get("id"):
1602
+ continue
1603
+ if self.current_pool and token_id == self.current_pool.get("token_id"):
1604
+ continue
1605
+
1606
+ (
1607
+ balance_status,
1608
+ balance_wei,
1609
+ ) = await self.balance_adapter.get_balance(
1610
+ token_id=token_id,
1611
+ wallet_address=self._get_vault_wallet_address(),
1612
+ )
1613
+ if not balance_status or not balance_wei:
1614
+ continue
1615
+ price = float(balance.get("price", 0.0))
1616
+ decimals = int(balance.get("decimals", 18))
1617
+ token_usd_value = price * float(balance_wei) / (10**decimals)
1618
+
1619
+ if token_usd_value > 1.0:
1620
+ needed_usd = usd_value - available_usdc_usd
1621
+ required_token_wei = int(
1622
+ math.ceil((needed_usd * (10**decimals)) / price)
1623
+ )
1624
+ amount_to_swap = min(required_token_wei, balance_wei)
1625
+ token_address = balance.get("tokenAddress")
1626
+ network = balance.get("network", "").lower()
1627
+ token_1_id = f"{network}_{token_address.lower()}"
1628
+ try:
1629
+ await self.brap_adapter.swap_from_token_ids(
1630
+ token_1_id,
1631
+ f"{self.usdc_token_info.get('chain').get('name')}_{self.usdc_token_info.get('address').lower()}",
1632
+ self._get_vault_wallet_address(),
1633
+ amount_to_swap,
1634
+ strategy_name=self.name,
1635
+ )
1636
+ swapped_usd = (amount_to_swap / (10**decimals)) * price
1637
+ available_usdc_usd += swapped_usd
1638
+ except Exception as e:
1639
+ logger.error(f"Error swapping {token_address} to USDC: {e}")
1640
+
1641
+ _, total_usdc_wei = await self.balance_adapter.get_balance(
1642
+ token_id=self.usdc_token_info.get("token_id"),
1643
+ wallet_address=self._get_vault_wallet_address(),
1644
+ )
1645
+ usdc_decimals = self.usdc_token_info.get("decimals")
1646
+ available_usdc_usd = float(total_usdc_wei or 0) / (10**usdc_decimals)
1647
+
1648
+ if (
1649
+ available_usdc_usd < usd_value
1650
+ and self.current_pool
1651
+ and self.current_pool.get("token_id")
1652
+ != self.usdc_token_info.get("token_id")
1653
+ ):
1654
+ remaining_usd = usd_value - available_usdc_usd
1655
+ (
1656
+ _,
1657
+ pool_balance_wei,
1658
+ ) = await self.balance_adapter.get_pool_balance(
1659
+ pool_address=self.current_pool.get("address"),
1660
+ chain_id=self.current_pool.get("chain").get("id"),
1661
+ user_address=self._get_vault_wallet_address(),
1662
+ )
1663
+ pool_decimals = self.current_pool.get("decimals")
1664
+ amount_to_swap = min(
1665
+ int(pool_balance_wei or 0), int(remaining_usd * (10**pool_decimals))
1666
+ )
1667
+
1668
+ try:
1669
+ await self.brap_adapter.swap_from_token_ids(
1670
+ f"{self.current_pool.get('chain').get('code').lower()}_{self.current_pool.get('address').lower()}",
1671
+ f"{self.usdc_token_info.get('chain').get('name')}_{self.usdc_token_info.get('address').lower()}",
1672
+ self._get_vault_wallet_address(),
1673
+ amount_to_swap,
1674
+ strategy_name=self.name,
1675
+ )
1676
+ except Exception as e:
1677
+ logger.error(f"Error swapping pool to USDC: {e}")
1678
+
1679
+ _, total_usdc_wei = await self.balance_adapter.get_balance(
1680
+ token_id=self.usdc_token_info.get("token_id"),
1681
+ wallet_address=self._get_vault_wallet_address(),
1682
+ )
1683
+ to_pay = min(int(total_usdc_wei or 0), int(usd_value * (10**usdc_decimals)))
1684
+ return (True, f"Partial liquidation completed. Paid {to_pay} USDC")