wayfinder-paths 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/CONFIG_GUIDE.md +394 -0
- wayfinder_paths/__init__.py +21 -0
- wayfinder_paths/config.example.json +20 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +13 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +90 -0
- wayfinder_paths/core/clients/ClientManager.py +231 -0
- wayfinder_paths/core/clients/HyperlendClient.py +151 -0
- wayfinder_paths/core/clients/LedgerClient.py +222 -0
- wayfinder_paths/core/clients/PoolClient.py +96 -0
- wayfinder_paths/core/clients/SimulationClient.py +180 -0
- wayfinder_paths/core/clients/TokenClient.py +73 -0
- wayfinder_paths/core/clients/TransactionClient.py +47 -0
- wayfinder_paths/core/clients/WalletClient.py +90 -0
- wayfinder_paths/core/clients/WayfinderClient.py +258 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +295 -0
- wayfinder_paths/core/clients/sdk_example.py +115 -0
- wayfinder_paths/core/config.py +369 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +25 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/VaultJob.py +182 -0
- wayfinder_paths/core/engine/__init__.py +5 -0
- wayfinder_paths/core/engine/manifest.py +97 -0
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +177 -0
- wayfinder_paths/core/services/local_evm_txn.py +429 -0
- wayfinder_paths/core/services/local_token_txn.py +231 -0
- wayfinder_paths/core/services/web3_service.py +45 -0
- wayfinder_paths/core/settings.py +61 -0
- wayfinder_paths/core/strategies/Strategy.py +183 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +165 -0
- wayfinder_paths/core/utils/wallets.py +77 -0
- wayfinder_paths/core/wallets/README.md +91 -0
- wayfinder_paths/core/wallets/WalletManager.py +56 -0
- wayfinder_paths/core/wallets/__init__.py +7 -0
- wayfinder_paths/run_strategy.py +409 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +160 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/tests/__init__.py +0 -0
- wayfinder_paths/tests/test_smoke_manifest.py +48 -0
- wayfinder_paths/tests/test_test_coverage.py +212 -0
- wayfinder_paths/tests/test_utils.py +64 -0
- wayfinder_paths/vaults/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
- wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
- wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
- wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
- wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
- wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
- wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
- wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
- wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
- wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
- wayfinder_paths/vaults/strategies/__init__.py +0 -0
- wayfinder_paths/vaults/strategies/config.py +85 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
- wayfinder_paths/vaults/templates/adapter/README.md +105 -0
- wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
- wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
- wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/vaults/templates/strategy/README.md +152 -0
- wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
- wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
- wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
- wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
- wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
- wayfinder_paths-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,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")
|