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,717 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from eth_utils import to_checksum_address
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
6
|
+
from wayfinder_paths.core.clients.BRAPClient import BRAPClient
|
|
7
|
+
from wayfinder_paths.core.clients.SimulationClient import SimulationClient
|
|
8
|
+
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
9
|
+
from wayfinder_paths.core.constants import DEFAULT_SLIPPAGE
|
|
10
|
+
from wayfinder_paths.core.services.base import Web3Service
|
|
11
|
+
from wayfinder_paths.core.settings import settings
|
|
12
|
+
from wayfinder_paths.vaults.adapters.ledger_adapter.adapter import LedgerAdapter
|
|
13
|
+
from wayfinder_paths.vaults.adapters.token_adapter.adapter import TokenAdapter
|
|
14
|
+
|
|
15
|
+
_NEEDS_CLEAR_APPROVAL = {
|
|
16
|
+
(1, "0xdac17f958d2ee523a2206206994597c13d831ec7"),
|
|
17
|
+
(137, "0xc2132d05d31c914a87c6611c10748aeb04b58e8f"),
|
|
18
|
+
(56, "0x55d398326f99059ff775485246999027b3197955"),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BRAPAdapter(BaseAdapter):
|
|
23
|
+
"""
|
|
24
|
+
BRAP (Bridge/Router/Adapter Protocol) adapter for cross-chain swaps and quotes.
|
|
25
|
+
|
|
26
|
+
Provides high-level operations for:
|
|
27
|
+
- Getting swap quotes across chains
|
|
28
|
+
- Executing cross-chain transactions
|
|
29
|
+
- Route optimization and fee calculation
|
|
30
|
+
- Bridge operations
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
adapter_type: str = "BRAP"
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
config: dict[str, Any] | None = None,
|
|
38
|
+
*,
|
|
39
|
+
web3_service: Web3Service,
|
|
40
|
+
simulation: bool = False,
|
|
41
|
+
):
|
|
42
|
+
super().__init__("brap_adapter", config)
|
|
43
|
+
self.brap_client = BRAPClient()
|
|
44
|
+
self.token_client = TokenClient()
|
|
45
|
+
self.token_adapter = TokenAdapter()
|
|
46
|
+
self.ledger_adapter = LedgerAdapter()
|
|
47
|
+
self.web3_service = web3_service
|
|
48
|
+
self.wallet_provider = web3_service.evm_transactions
|
|
49
|
+
self.token_transactions = web3_service.token_transactions
|
|
50
|
+
self.simulation = simulation
|
|
51
|
+
self.simulation_client = SimulationClient() if simulation else None
|
|
52
|
+
|
|
53
|
+
async def get_swap_quote(
|
|
54
|
+
self,
|
|
55
|
+
from_token_address: str,
|
|
56
|
+
to_token_address: str,
|
|
57
|
+
from_chain_id: int,
|
|
58
|
+
to_chain_id: int,
|
|
59
|
+
from_address: str,
|
|
60
|
+
to_address: str,
|
|
61
|
+
amount: str,
|
|
62
|
+
slippage: float | None = None,
|
|
63
|
+
wayfinder_fee: float | None = None,
|
|
64
|
+
) -> tuple[bool, Any]:
|
|
65
|
+
"""
|
|
66
|
+
Get a quote for a cross-chain swap operation.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
from_token_address: Source token contract address
|
|
70
|
+
to_token_address: Destination token contract address
|
|
71
|
+
from_chain_id: Source chain ID
|
|
72
|
+
to_chain_id: Destination chain ID
|
|
73
|
+
from_address: Source wallet address
|
|
74
|
+
to_address: Destination wallet address
|
|
75
|
+
amount: Amount to swap (in smallest units)
|
|
76
|
+
slippage: Maximum slippage tolerance (optional)
|
|
77
|
+
wayfinder_fee: Wayfinder fee (optional)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Tuple of (success, data) where data is quote information or error message
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
data = await self.brap_client.get_quote(
|
|
84
|
+
from_token_address=from_token_address,
|
|
85
|
+
to_token_address=to_token_address,
|
|
86
|
+
from_chain_id=from_chain_id,
|
|
87
|
+
to_chain_id=to_chain_id,
|
|
88
|
+
from_address=from_address,
|
|
89
|
+
to_address=to_address,
|
|
90
|
+
amount1=amount,
|
|
91
|
+
slippage=slippage,
|
|
92
|
+
wayfinder_fee=wayfinder_fee,
|
|
93
|
+
)
|
|
94
|
+
return (True, data)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
self.logger.error(f"Error getting swap quote: {e}")
|
|
97
|
+
return (False, str(e))
|
|
98
|
+
|
|
99
|
+
async def get_best_quote(
|
|
100
|
+
self,
|
|
101
|
+
from_token_address: str,
|
|
102
|
+
to_token_address: str,
|
|
103
|
+
from_chain_id: int,
|
|
104
|
+
to_chain_id: int,
|
|
105
|
+
from_address: str,
|
|
106
|
+
to_address: str,
|
|
107
|
+
amount: str,
|
|
108
|
+
slippage: float | None = None,
|
|
109
|
+
wayfinder_fee: float | None = None,
|
|
110
|
+
) -> tuple[bool, Any]:
|
|
111
|
+
"""
|
|
112
|
+
Get the best available quote for a swap operation.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
from_token_address: Source token contract address
|
|
116
|
+
to_token_address: Destination token contract address
|
|
117
|
+
from_chain_id: Source chain ID
|
|
118
|
+
to_chain_id: Destination chain ID
|
|
119
|
+
from_address: Source wallet address
|
|
120
|
+
to_address: Destination wallet address
|
|
121
|
+
amount: Amount to swap (in smallest units)
|
|
122
|
+
slippage: Maximum slippage tolerance (optional)
|
|
123
|
+
wayfinder_fee: Wayfinder fee (optional)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Tuple of (success, data) where data is best quote or error message
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
data = await self.brap_client.get_quote(
|
|
130
|
+
from_token_address=from_token_address,
|
|
131
|
+
to_token_address=to_token_address,
|
|
132
|
+
from_chain_id=from_chain_id,
|
|
133
|
+
to_chain_id=to_chain_id,
|
|
134
|
+
from_address=from_address,
|
|
135
|
+
to_address=to_address,
|
|
136
|
+
amount1=amount,
|
|
137
|
+
slippage=slippage,
|
|
138
|
+
wayfinder_fee=wayfinder_fee,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Extract best quote from response
|
|
142
|
+
quotes = data.get("quotes", {})
|
|
143
|
+
best_quote = quotes.get("best_quote")
|
|
144
|
+
|
|
145
|
+
if not best_quote:
|
|
146
|
+
return (False, "No quotes available")
|
|
147
|
+
|
|
148
|
+
return (True, best_quote)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
self.logger.error(f"Error getting best quote: {e}")
|
|
151
|
+
return (False, str(e))
|
|
152
|
+
|
|
153
|
+
async def calculate_swap_fees(
|
|
154
|
+
self,
|
|
155
|
+
from_token_address: str,
|
|
156
|
+
to_token_address: str,
|
|
157
|
+
from_chain_id: int,
|
|
158
|
+
to_chain_id: int,
|
|
159
|
+
amount: str,
|
|
160
|
+
slippage: float | None = None,
|
|
161
|
+
) -> tuple[bool, Any]:
|
|
162
|
+
"""
|
|
163
|
+
Calculate fees for a swap operation.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
from_token_address: Source token contract address
|
|
167
|
+
to_token_address: Destination token contract address
|
|
168
|
+
from_chain_id: Source chain ID
|
|
169
|
+
to_chain_id: Destination chain ID
|
|
170
|
+
amount: Amount to swap (in smallest units)
|
|
171
|
+
slippage: Maximum slippage tolerance (optional)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Tuple of (success, data) where data is fee breakdown or error message
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
# Get quote to extract fee information
|
|
178
|
+
success, quote_data = await self.get_swap_quote(
|
|
179
|
+
from_token_address=from_token_address,
|
|
180
|
+
to_token_address=to_token_address,
|
|
181
|
+
from_chain_id=from_chain_id,
|
|
182
|
+
to_chain_id=to_chain_id,
|
|
183
|
+
from_address="0x0000000000000000000000000000000000000000", # Dummy address
|
|
184
|
+
to_address="0x0000000000000000000000000000000000000000", # Dummy address
|
|
185
|
+
amount=amount,
|
|
186
|
+
slippage=slippage,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if not success:
|
|
190
|
+
return (False, quote_data)
|
|
191
|
+
|
|
192
|
+
quotes = quote_data.get("quotes", {})
|
|
193
|
+
best_quote = quotes.get("best_quote")
|
|
194
|
+
|
|
195
|
+
if not best_quote:
|
|
196
|
+
return (False, "No quote available for fee calculation")
|
|
197
|
+
|
|
198
|
+
# Extract fee information
|
|
199
|
+
fees = {
|
|
200
|
+
"input_amount": best_quote.get("input_amount", 0),
|
|
201
|
+
"output_amount": best_quote.get("output_amount", 0),
|
|
202
|
+
"gas_fee": best_quote.get("gas_fee", 0),
|
|
203
|
+
"bridge_fee": best_quote.get("bridge_fee", 0),
|
|
204
|
+
"protocol_fee": best_quote.get("protocol_fee", 0),
|
|
205
|
+
"total_fee": best_quote.get("total_fee", 0),
|
|
206
|
+
"slippage": best_quote.get("slippage", 0),
|
|
207
|
+
"price_impact": best_quote.get("price_impact", 0),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (True, fees)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
self.logger.error(f"Error calculating swap fees: {e}")
|
|
213
|
+
return (False, str(e))
|
|
214
|
+
|
|
215
|
+
async def compare_routes(
|
|
216
|
+
self,
|
|
217
|
+
from_token_address: str,
|
|
218
|
+
to_token_address: str,
|
|
219
|
+
from_chain_id: int,
|
|
220
|
+
to_chain_id: int,
|
|
221
|
+
amount: str,
|
|
222
|
+
slippage: float | None = None,
|
|
223
|
+
) -> tuple[bool, Any]:
|
|
224
|
+
"""
|
|
225
|
+
Compare multiple routes for a swap operation.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
from_token_address: Source token contract address
|
|
229
|
+
to_token_address: Destination token contract address
|
|
230
|
+
from_chain_id: Source chain ID
|
|
231
|
+
to_chain_id: Destination chain ID
|
|
232
|
+
amount: Amount to swap (in smallest units)
|
|
233
|
+
slippage: Maximum slippage tolerance (optional)
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Tuple of (success, data) where data is route comparison or error message
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
data = await self.brap_client.get_quote(
|
|
240
|
+
from_token_address=from_token_address,
|
|
241
|
+
to_token_address=to_token_address,
|
|
242
|
+
from_chain_id=from_chain_id,
|
|
243
|
+
to_chain_id=to_chain_id,
|
|
244
|
+
from_address="0x0000000000000000000000000000000000000000", # Dummy address
|
|
245
|
+
to_address="0x0000000000000000000000000000000000000000", # Dummy address
|
|
246
|
+
amount1=amount,
|
|
247
|
+
slippage=slippage,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
quotes = data.get("quotes", {})
|
|
251
|
+
all_quotes = quotes.get("quotes", [])
|
|
252
|
+
best_quote = quotes.get("best_quote")
|
|
253
|
+
|
|
254
|
+
if not all_quotes:
|
|
255
|
+
return (False, "No routes available")
|
|
256
|
+
|
|
257
|
+
# Sort quotes by output amount (descending)
|
|
258
|
+
sorted_quotes = sorted(
|
|
259
|
+
all_quotes, key=lambda x: int(x.get("output_amount", 0)), reverse=True
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
comparison = {
|
|
263
|
+
"total_routes": len(all_quotes),
|
|
264
|
+
"best_route": best_quote,
|
|
265
|
+
"all_routes": sorted_quotes,
|
|
266
|
+
"route_analysis": {
|
|
267
|
+
"highest_output": sorted_quotes[0] if sorted_quotes else None,
|
|
268
|
+
"lowest_fees": min(
|
|
269
|
+
all_quotes, key=lambda x: int(x.get("total_fee", 0))
|
|
270
|
+
)
|
|
271
|
+
if all_quotes
|
|
272
|
+
else None,
|
|
273
|
+
"fastest": min(
|
|
274
|
+
all_quotes, key=lambda x: int(x.get("estimated_time", 0))
|
|
275
|
+
)
|
|
276
|
+
if all_quotes
|
|
277
|
+
else None,
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (True, comparison)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
self.logger.error(f"Error comparing routes: {e}")
|
|
284
|
+
return (False, str(e))
|
|
285
|
+
|
|
286
|
+
async def swap_from_token_ids(
|
|
287
|
+
self,
|
|
288
|
+
from_token_id: str,
|
|
289
|
+
to_token_id: str,
|
|
290
|
+
from_address: str,
|
|
291
|
+
amount: str,
|
|
292
|
+
slippage: float = DEFAULT_SLIPPAGE,
|
|
293
|
+
strategy_name: str | None = None,
|
|
294
|
+
) -> tuple[bool, Any]:
|
|
295
|
+
"""
|
|
296
|
+
Execute a swap by looking up token metadata via token IDs.
|
|
297
|
+
"""
|
|
298
|
+
from_token = await self.token_client.get_token_details(from_token_id)
|
|
299
|
+
if not from_token:
|
|
300
|
+
return (False, f"From token not found: {from_token_id}")
|
|
301
|
+
to_token = await self.token_client.get_token_details(to_token_id)
|
|
302
|
+
if not to_token:
|
|
303
|
+
return (False, f"To token not found: {to_token_id}")
|
|
304
|
+
|
|
305
|
+
success, best_quote = await self.get_best_quote(
|
|
306
|
+
from_token_address=from_token.get("address"),
|
|
307
|
+
to_token_address=to_token.get("address"),
|
|
308
|
+
from_chain_id=(from_token.get("chain") or {}).get("id"),
|
|
309
|
+
to_chain_id=(to_token.get("chain") or {}).get("id"),
|
|
310
|
+
from_address=from_address,
|
|
311
|
+
to_address=from_address,
|
|
312
|
+
amount=amount,
|
|
313
|
+
slippage=slippage,
|
|
314
|
+
)
|
|
315
|
+
if not success:
|
|
316
|
+
return (False, best_quote)
|
|
317
|
+
|
|
318
|
+
return await self.swap_from_quote(
|
|
319
|
+
from_token=from_token,
|
|
320
|
+
to_token=to_token,
|
|
321
|
+
from_address=from_address,
|
|
322
|
+
quote=best_quote,
|
|
323
|
+
strategy_name=strategy_name,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
async def swap_from_quote(
|
|
327
|
+
self,
|
|
328
|
+
from_token: dict[str, Any],
|
|
329
|
+
to_token: dict[str, Any],
|
|
330
|
+
from_address: str,
|
|
331
|
+
quote: dict[str, Any],
|
|
332
|
+
strategy_name: str | None = None,
|
|
333
|
+
) -> tuple[bool, Any]:
|
|
334
|
+
"""
|
|
335
|
+
Execute a swap using a previously retrieved BRAP quote.
|
|
336
|
+
"""
|
|
337
|
+
chain = from_token.get("chain") or {}
|
|
338
|
+
chain_id = self._chain_id(chain)
|
|
339
|
+
|
|
340
|
+
transaction = dict(quote.get("calldata") or {})
|
|
341
|
+
if not transaction:
|
|
342
|
+
return (False, "Quote missing calldata")
|
|
343
|
+
transaction["chainId"] = chain_id
|
|
344
|
+
transaction["from"] = to_checksum_address(from_address)
|
|
345
|
+
|
|
346
|
+
spender = transaction.get("to")
|
|
347
|
+
approve_amount = (
|
|
348
|
+
quote.get("input_amount")
|
|
349
|
+
or quote.get("inputAmount")
|
|
350
|
+
or transaction.get("value")
|
|
351
|
+
)
|
|
352
|
+
if from_token.get("address") and spender and approve_amount:
|
|
353
|
+
approve_success, approve_response = await self._handle_token_approval(
|
|
354
|
+
chain=chain,
|
|
355
|
+
token_address=from_token.get("address"),
|
|
356
|
+
owner_address=from_address,
|
|
357
|
+
spender_address=spender,
|
|
358
|
+
amount=int(approve_amount),
|
|
359
|
+
)
|
|
360
|
+
if not approve_success:
|
|
361
|
+
return (False, approve_response)
|
|
362
|
+
|
|
363
|
+
if self.simulation:
|
|
364
|
+
simulation = await self._simulate_swap(
|
|
365
|
+
from_token, to_token, from_address, chain_id, quote
|
|
366
|
+
)
|
|
367
|
+
return (True, {"quote": quote, "simulation": simulation})
|
|
368
|
+
|
|
369
|
+
broadcast_success, broadcast_response = await self._broadcast_transaction(
|
|
370
|
+
transaction
|
|
371
|
+
)
|
|
372
|
+
if not broadcast_success:
|
|
373
|
+
return (False, broadcast_response)
|
|
374
|
+
|
|
375
|
+
ledger_record = await self._record_swap_operation(
|
|
376
|
+
from_token=from_token,
|
|
377
|
+
to_token=to_token,
|
|
378
|
+
wallet_address=from_address,
|
|
379
|
+
quote=quote,
|
|
380
|
+
broadcast_response=broadcast_response,
|
|
381
|
+
strategy_name=strategy_name,
|
|
382
|
+
)
|
|
383
|
+
return (True, ledger_record)
|
|
384
|
+
|
|
385
|
+
async def get_bridge_quote(
|
|
386
|
+
self,
|
|
387
|
+
from_token_address: str,
|
|
388
|
+
to_token_address: str,
|
|
389
|
+
from_chain_id: int,
|
|
390
|
+
to_chain_id: int,
|
|
391
|
+
amount: str,
|
|
392
|
+
slippage: float | None = None,
|
|
393
|
+
) -> tuple[bool, Any]:
|
|
394
|
+
"""
|
|
395
|
+
Get a quote for a bridge operation (same as swap for BRAP).
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
from_token_address: Source token contract address
|
|
399
|
+
to_token_address: Destination token contract address
|
|
400
|
+
from_chain_id: Source chain ID
|
|
401
|
+
to_chain_id: Destination chain ID
|
|
402
|
+
amount: Amount to bridge (in smallest units)
|
|
403
|
+
slippage: Maximum slippage tolerance (optional)
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Tuple of (success, data) where data is bridge quote or error message
|
|
407
|
+
"""
|
|
408
|
+
# For BRAP, bridge operations are the same as swap operations
|
|
409
|
+
return await self.get_swap_quote(
|
|
410
|
+
from_token_address=from_token_address,
|
|
411
|
+
to_token_address=to_token_address,
|
|
412
|
+
from_chain_id=from_chain_id,
|
|
413
|
+
to_chain_id=to_chain_id,
|
|
414
|
+
from_address="0x0000000000000000000000000000000000000000", # Dummy address
|
|
415
|
+
to_address="0x0000000000000000000000000000000000000000", # Dummy address
|
|
416
|
+
amount=amount,
|
|
417
|
+
slippage=slippage,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
async def estimate_gas_cost(
|
|
421
|
+
self, from_chain_id: int, to_chain_id: int, operation_type: str = "swap"
|
|
422
|
+
) -> tuple[bool, Any]:
|
|
423
|
+
"""
|
|
424
|
+
Estimate gas costs for a cross-chain operation.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
from_chain_id: Source chain ID
|
|
428
|
+
to_chain_id: Destination chain ID
|
|
429
|
+
operation_type: Type of operation ("swap", "bridge")
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Tuple of (success, data) where data is gas cost estimate or error message
|
|
433
|
+
"""
|
|
434
|
+
try:
|
|
435
|
+
# This is a simplified estimation - in practice, you'd want to
|
|
436
|
+
# query actual gas prices from the chains
|
|
437
|
+
gas_estimates = {
|
|
438
|
+
"ethereum": {"swap": 150000, "bridge": 200000},
|
|
439
|
+
"base": {"swap": 100000, "bridge": 150000},
|
|
440
|
+
"arbitrum": {"swap": 80000, "bridge": 120000},
|
|
441
|
+
"polygon": {"swap": 60000, "bridge": 100000},
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# Map chain IDs to names (simplified)
|
|
445
|
+
chain_names = {
|
|
446
|
+
1: "ethereum",
|
|
447
|
+
8453: "base",
|
|
448
|
+
42161: "arbitrum",
|
|
449
|
+
137: "polygon",
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
from_chain = chain_names.get(from_chain_id, "unknown")
|
|
453
|
+
to_chain = chain_names.get(to_chain_id, "unknown")
|
|
454
|
+
|
|
455
|
+
from_gas = gas_estimates.get(from_chain, {}).get(operation_type, 100000)
|
|
456
|
+
to_gas = gas_estimates.get(to_chain, {}).get(operation_type, 100000)
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
True,
|
|
460
|
+
{
|
|
461
|
+
"from_chain": from_chain,
|
|
462
|
+
"to_chain": to_chain,
|
|
463
|
+
"from_gas_estimate": from_gas,
|
|
464
|
+
"to_gas_estimate": to_gas,
|
|
465
|
+
"total_operations": 2 if from_chain_id != to_chain_id else 1,
|
|
466
|
+
"operation_type": operation_type,
|
|
467
|
+
},
|
|
468
|
+
)
|
|
469
|
+
except Exception as e:
|
|
470
|
+
self.logger.error(f"Error estimating gas cost: {e}")
|
|
471
|
+
return (False, str(e))
|
|
472
|
+
|
|
473
|
+
async def validate_swap_parameters(
|
|
474
|
+
self,
|
|
475
|
+
from_token_address: str,
|
|
476
|
+
to_token_address: str,
|
|
477
|
+
from_chain_id: int,
|
|
478
|
+
to_chain_id: int,
|
|
479
|
+
amount: str,
|
|
480
|
+
) -> tuple[bool, Any]:
|
|
481
|
+
"""
|
|
482
|
+
Validate swap parameters before executing.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
from_token_address: Source token contract address
|
|
486
|
+
to_token_address: Destination token contract address
|
|
487
|
+
from_chain_id: Source chain ID
|
|
488
|
+
to_chain_id: Destination chain ID
|
|
489
|
+
amount: Amount to swap (in smallest units)
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Tuple of (success, data) where data is validation result or error message
|
|
493
|
+
"""
|
|
494
|
+
try:
|
|
495
|
+
validation_errors = []
|
|
496
|
+
|
|
497
|
+
# Basic validation
|
|
498
|
+
if not from_token_address or len(from_token_address) != 42:
|
|
499
|
+
validation_errors.append("Invalid from_token_address")
|
|
500
|
+
|
|
501
|
+
if not to_token_address or len(to_token_address) != 42:
|
|
502
|
+
validation_errors.append("Invalid to_token_address")
|
|
503
|
+
|
|
504
|
+
if from_chain_id <= 0 or to_chain_id <= 0:
|
|
505
|
+
validation_errors.append("Invalid chain IDs")
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
amount_int = int(amount)
|
|
509
|
+
if amount_int <= 0:
|
|
510
|
+
validation_errors.append("Amount must be positive")
|
|
511
|
+
except (ValueError, TypeError):
|
|
512
|
+
validation_errors.append("Invalid amount format")
|
|
513
|
+
|
|
514
|
+
if validation_errors:
|
|
515
|
+
return (False, {"valid": False, "errors": validation_errors})
|
|
516
|
+
|
|
517
|
+
# Try to get a quote to validate the swap is possible
|
|
518
|
+
success, quote_data = await self.get_swap_quote(
|
|
519
|
+
from_token_address=from_token_address,
|
|
520
|
+
to_token_address=to_token_address,
|
|
521
|
+
from_chain_id=from_chain_id,
|
|
522
|
+
to_chain_id=to_chain_id,
|
|
523
|
+
from_address="0x0000000000000000000000000000000000000000",
|
|
524
|
+
to_address="0x0000000000000000000000000000000000000000",
|
|
525
|
+
amount=amount,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
if not success:
|
|
529
|
+
validation_errors.append(f"Swap not possible: {quote_data}")
|
|
530
|
+
return (False, {"valid": False, "errors": validation_errors})
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
True,
|
|
534
|
+
{
|
|
535
|
+
"valid": True,
|
|
536
|
+
"quote_available": True,
|
|
537
|
+
"estimated_output": quote_data.get("quotes", {})
|
|
538
|
+
.get("best_quote", {})
|
|
539
|
+
.get("output_amount", "0"),
|
|
540
|
+
},
|
|
541
|
+
)
|
|
542
|
+
except Exception as e:
|
|
543
|
+
self.logger.error(f"Error validating swap parameters: {e}")
|
|
544
|
+
return (False, str(e))
|
|
545
|
+
|
|
546
|
+
async def _handle_token_approval(
|
|
547
|
+
self,
|
|
548
|
+
*,
|
|
549
|
+
chain: dict[str, Any],
|
|
550
|
+
token_address: str,
|
|
551
|
+
owner_address: str,
|
|
552
|
+
spender_address: str,
|
|
553
|
+
amount: int,
|
|
554
|
+
) -> tuple[bool, Any]:
|
|
555
|
+
chain_id = self._chain_id(chain)
|
|
556
|
+
token_checksum = to_checksum_address(token_address)
|
|
557
|
+
owner_checksum = to_checksum_address(owner_address)
|
|
558
|
+
spender_checksum = to_checksum_address(spender_address)
|
|
559
|
+
|
|
560
|
+
if (chain_id, token_checksum.lower()) in _NEEDS_CLEAR_APPROVAL:
|
|
561
|
+
allowance = await self.token_transactions.read_erc20_allowance(
|
|
562
|
+
{"id": chain_id},
|
|
563
|
+
token_checksum,
|
|
564
|
+
owner_checksum,
|
|
565
|
+
spender_checksum,
|
|
566
|
+
)
|
|
567
|
+
if allowance.get("allowance", 0) > 0:
|
|
568
|
+
clear_success, clear_tx = self.token_transactions.build_erc20_approve(
|
|
569
|
+
chain_id=chain_id,
|
|
570
|
+
token_address=token_checksum,
|
|
571
|
+
from_address=owner_checksum,
|
|
572
|
+
spender=spender_checksum,
|
|
573
|
+
amount=0,
|
|
574
|
+
)
|
|
575
|
+
if not clear_success:
|
|
576
|
+
return False, clear_tx
|
|
577
|
+
clear_result = await self._broadcast_transaction(clear_tx)
|
|
578
|
+
if not clear_result[0]:
|
|
579
|
+
return clear_result
|
|
580
|
+
|
|
581
|
+
build_success, approve_tx = self.token_transactions.build_erc20_approve(
|
|
582
|
+
chain_id=chain_id,
|
|
583
|
+
token_address=token_checksum,
|
|
584
|
+
from_address=owner_checksum,
|
|
585
|
+
spender=spender_checksum,
|
|
586
|
+
amount=int(amount),
|
|
587
|
+
)
|
|
588
|
+
if not build_success:
|
|
589
|
+
return False, approve_tx
|
|
590
|
+
return await self._broadcast_transaction(approve_tx)
|
|
591
|
+
|
|
592
|
+
async def _broadcast_transaction(
|
|
593
|
+
self, transaction: dict[str, Any]
|
|
594
|
+
) -> tuple[bool, Any]:
|
|
595
|
+
if getattr(settings, "DRY_RUN", False):
|
|
596
|
+
return True, {"dry_run": True, "transaction": transaction}
|
|
597
|
+
return await self.wallet_provider.broadcast_transaction(
|
|
598
|
+
transaction,
|
|
599
|
+
wait_for_receipt=True,
|
|
600
|
+
timeout=120,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
async def _record_swap_operation(
|
|
604
|
+
self,
|
|
605
|
+
from_token: dict[str, Any],
|
|
606
|
+
to_token: dict[str, Any],
|
|
607
|
+
wallet_address: str,
|
|
608
|
+
quote: dict[str, Any],
|
|
609
|
+
broadcast_response: dict[str, Any] | Any,
|
|
610
|
+
strategy_name: str | None = None,
|
|
611
|
+
) -> Any:
|
|
612
|
+
from_amount_usd = quote.get("from_amount_usd")
|
|
613
|
+
if from_amount_usd is None:
|
|
614
|
+
from_amount_usd = await self._token_amount_usd(
|
|
615
|
+
from_token, quote.get("input_amount")
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
to_amount_usd = quote.get("to_amount_usd")
|
|
619
|
+
if to_amount_usd is None:
|
|
620
|
+
to_amount_usd = await self._token_amount_usd(
|
|
621
|
+
to_token, quote.get("output_amount")
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
response = broadcast_response if isinstance(broadcast_response, dict) else {}
|
|
625
|
+
operation_payload = {
|
|
626
|
+
"type": "SWAP",
|
|
627
|
+
"from_token_id": from_token.get("id"),
|
|
628
|
+
"to_token_id": to_token.get("id"),
|
|
629
|
+
"from_amount": quote.get("input_amount"),
|
|
630
|
+
"to_amount": quote.get("output_amount"),
|
|
631
|
+
"from_amount_usd": from_amount_usd or 0,
|
|
632
|
+
"to_amount_usd": to_amount_usd or 0,
|
|
633
|
+
"transaction_hash": response.get("transaction_hash"),
|
|
634
|
+
"transaction_status": response.get("transaction_status"),
|
|
635
|
+
"transaction_receipt": response.get("transaction_receipt"),
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
try:
|
|
639
|
+
success, ledger_response = await self.ledger_adapter.record_operation(
|
|
640
|
+
wallet_address=wallet_address,
|
|
641
|
+
operation_data=operation_payload,
|
|
642
|
+
usd_value=from_amount_usd or 0,
|
|
643
|
+
strategy_name=strategy_name,
|
|
644
|
+
)
|
|
645
|
+
if success:
|
|
646
|
+
return ledger_response
|
|
647
|
+
self.logger.warning(
|
|
648
|
+
"Ledger swap record failed", error=ledger_response, quote=quote
|
|
649
|
+
)
|
|
650
|
+
except Exception as exc: # noqa: BLE001
|
|
651
|
+
self.logger.warning(f"Ledger swap record raised: {exc}", quote=quote)
|
|
652
|
+
|
|
653
|
+
return operation_payload
|
|
654
|
+
|
|
655
|
+
async def _token_amount_usd(
|
|
656
|
+
self, token_info: dict[str, Any], raw_amount: Any
|
|
657
|
+
) -> float | None:
|
|
658
|
+
if raw_amount is None:
|
|
659
|
+
return None
|
|
660
|
+
success, price_data = await self.token_adapter.get_token_price(
|
|
661
|
+
token_info.get("id")
|
|
662
|
+
)
|
|
663
|
+
if not success or not price_data:
|
|
664
|
+
return None
|
|
665
|
+
decimals = token_info.get("decimals") or 18
|
|
666
|
+
return (
|
|
667
|
+
price_data.get("current_price", 0.0)
|
|
668
|
+
* float(raw_amount)
|
|
669
|
+
/ 10 ** int(decimals)
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
async def _simulate_swap(
|
|
673
|
+
self,
|
|
674
|
+
from_token: dict[str, Any],
|
|
675
|
+
to_token: dict[str, Any],
|
|
676
|
+
from_address: str,
|
|
677
|
+
chain_id: int,
|
|
678
|
+
quote: dict[str, Any],
|
|
679
|
+
) -> Any:
|
|
680
|
+
client = await self._get_simulation_client()
|
|
681
|
+
initial_balances = {"native": "5000000000000000000"}
|
|
682
|
+
if from_token.get("address"):
|
|
683
|
+
initial_balances[from_token.get("address")] = "1000000000000000000000000"
|
|
684
|
+
|
|
685
|
+
slippage = quote.get("slippage") or quote.get("slippage_percent")
|
|
686
|
+
if isinstance(slippage, str):
|
|
687
|
+
try:
|
|
688
|
+
slippage = float(slippage)
|
|
689
|
+
except ValueError:
|
|
690
|
+
slippage = DEFAULT_SLIPPAGE
|
|
691
|
+
slippage = slippage or DEFAULT_SLIPPAGE
|
|
692
|
+
|
|
693
|
+
amount = quote.get("input_amount") or quote.get("inputAmount") or "0"
|
|
694
|
+
return await client.simulate_swap(
|
|
695
|
+
from_token_address=from_token.get("address"),
|
|
696
|
+
to_token_address=to_token.get("address"),
|
|
697
|
+
from_chain_id=chain_id,
|
|
698
|
+
to_chain_id=chain_id,
|
|
699
|
+
amount=str(amount),
|
|
700
|
+
from_address=from_address,
|
|
701
|
+
slippage=float(slippage),
|
|
702
|
+
initial_balances=initial_balances,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
async def _get_simulation_client(self) -> SimulationClient:
|
|
706
|
+
if not self.simulation_client:
|
|
707
|
+
self.simulation_client = SimulationClient()
|
|
708
|
+
return self.simulation_client
|
|
709
|
+
|
|
710
|
+
def _chain_id(self, chain: Any) -> int:
|
|
711
|
+
if isinstance(chain, dict):
|
|
712
|
+
chain_id = chain.get("id") or chain.get("chain_id")
|
|
713
|
+
else:
|
|
714
|
+
chain_id = getattr(chain, "id", None)
|
|
715
|
+
if chain_id is None:
|
|
716
|
+
raise ValueError("Chain ID is required")
|
|
717
|
+
return int(chain_id)
|