wayfinder-paths 0.1.7__py3-none-any.whl

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

Potentially problematic release.


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

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