wayfinder-paths 0.1.21__py3-none-any.whl → 0.1.23__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 (63) hide show
  1. wayfinder_paths/__init__.py +0 -4
  2. wayfinder_paths/adapters/balance_adapter/README.md +0 -1
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +65 -169
  4. wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -113
  5. wayfinder_paths/adapters/brap_adapter/README.md +22 -75
  6. wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
  7. wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
  8. wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
  9. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +39 -86
  10. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +5 -1
  11. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +6 -5
  12. wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
  13. wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
  14. wayfinder_paths/adapters/moonwell_adapter/adapter.py +108 -198
  15. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +37 -23
  16. wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
  17. wayfinder_paths/core/__init__.py +0 -3
  18. wayfinder_paths/core/clients/BRAPClient.py +3 -0
  19. wayfinder_paths/core/clients/ClientManager.py +0 -7
  20. wayfinder_paths/core/clients/LedgerClient.py +196 -172
  21. wayfinder_paths/core/clients/WayfinderClient.py +0 -1
  22. wayfinder_paths/core/clients/__init__.py +0 -5
  23. wayfinder_paths/core/clients/protocols.py +0 -13
  24. wayfinder_paths/core/config.py +0 -164
  25. wayfinder_paths/core/constants/__init__.py +58 -2
  26. wayfinder_paths/core/constants/base.py +8 -22
  27. wayfinder_paths/core/constants/chains.py +36 -0
  28. wayfinder_paths/core/constants/contracts.py +39 -0
  29. wayfinder_paths/core/constants/tokens.py +9 -0
  30. wayfinder_paths/core/strategies/Strategy.py +0 -10
  31. wayfinder_paths/core/utils/evm_helpers.py +5 -15
  32. wayfinder_paths/core/utils/tokens.py +28 -0
  33. wayfinder_paths/core/utils/transaction.py +13 -7
  34. wayfinder_paths/core/utils/web3.py +5 -3
  35. wayfinder_paths/policies/enso.py +1 -2
  36. wayfinder_paths/policies/hyper_evm.py +6 -3
  37. wayfinder_paths/policies/hyperlend.py +1 -2
  38. wayfinder_paths/policies/moonwell.py +12 -7
  39. wayfinder_paths/policies/prjx.py +1 -3
  40. wayfinder_paths/run_strategy.py +97 -300
  41. wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
  42. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +19 -14
  43. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +12 -11
  44. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +20 -33
  45. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +21 -18
  46. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -130
  47. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +32 -42
  48. {wayfinder_paths-0.1.21.dist-info → wayfinder_paths-0.1.23.dist-info}/METADATA +3 -4
  49. {wayfinder_paths-0.1.21.dist-info → wayfinder_paths-0.1.23.dist-info}/RECORD +51 -60
  50. {wayfinder_paths-0.1.21.dist-info → wayfinder_paths-0.1.23.dist-info}/WHEEL +1 -1
  51. wayfinder_paths/core/clients/WalletClient.py +0 -41
  52. wayfinder_paths/core/engine/StrategyJob.py +0 -110
  53. wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
  54. wayfinder_paths/templates/adapter/README.md +0 -150
  55. wayfinder_paths/templates/adapter/adapter.py +0 -16
  56. wayfinder_paths/templates/adapter/examples.json +0 -8
  57. wayfinder_paths/templates/adapter/test_adapter.py +0 -30
  58. wayfinder_paths/templates/strategy/README.md +0 -186
  59. wayfinder_paths/templates/strategy/examples.json +0 -11
  60. wayfinder_paths/templates/strategy/strategy.py +0 -35
  61. wayfinder_paths/templates/strategy/test_strategy.py +0 -166
  62. wayfinder_paths/tests/test_smoke_manifest.py +0 -63
  63. {wayfinder_paths-0.1.21.dist-info → wayfinder_paths-0.1.23.dist-info}/LICENSE +0 -0
@@ -2,31 +2,23 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
- from eth_utils import to_checksum_address
5
+ from web3 import Web3
6
6
 
7
7
  from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
8
8
  from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
9
9
  from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
10
10
  from wayfinder_paths.core.adapters.models import SWAP
11
- from wayfinder_paths.core.clients.BRAPClient import (
12
- BRAPClient,
13
- BRAPQuoteResponse,
14
- )
11
+ from wayfinder_paths.core.clients.BRAPClient import BRAPClient
15
12
  from wayfinder_paths.core.clients.LedgerClient import TransactionRecord
16
13
  from wayfinder_paths.core.clients.TokenClient import TokenClient
17
- from wayfinder_paths.core.constants import DEFAULT_SLIPPAGE, ZERO_ADDRESS
14
+ from wayfinder_paths.core.constants.contracts import TOKENS_REQUIRING_APPROVAL_RESET
18
15
  from wayfinder_paths.core.utils.tokens import (
19
16
  build_approve_transaction,
20
17
  get_token_allowance,
18
+ is_native_token,
21
19
  )
22
20
  from wayfinder_paths.core.utils.transaction import send_transaction
23
21
 
24
- _NEEDS_CLEAR_APPROVAL = {
25
- (1, "0xdac17f958d2ee523a2206206994597c13d831ec7"),
26
- (137, "0xc2132d05d31c914a87c6611c10748aeb04b58e8f"),
27
- (56, "0x55d398326f99059ff775485246999027b3197955"),
28
- }
29
-
30
22
 
31
23
  class BRAPAdapter(BaseAdapter):
32
24
  adapter_type: str = "BRAP"
@@ -43,280 +35,161 @@ class BRAPAdapter(BaseAdapter):
43
35
  self.token_adapter = TokenAdapter()
44
36
  self.ledger_adapter = LedgerAdapter()
45
37
 
46
- async def get_swap_quote(
47
- self,
48
- from_token_address: str,
49
- to_token_address: str,
50
- from_chain_id: int,
51
- to_chain_id: int,
52
- from_address: str,
53
- to_address: str,
54
- amount: str,
55
- slippage: float | None = None,
56
- wayfinder_fee: float | None = None,
57
- ) -> tuple[bool, BRAPQuoteResponse | str]:
58
- try:
59
- data = await self.brap_client.get_quote(
60
- from_token=from_token_address,
61
- to_token=to_token_address,
62
- from_chain=from_chain_id,
63
- to_chain=to_chain_id,
64
- from_wallet=from_address,
65
- from_amount=amount,
66
- )
67
- return (True, data)
68
- except Exception as e:
69
- self.logger.error(f"Error getting swap quote: {e}")
70
- return (False, str(e))
71
-
72
- async def get_best_quote(
73
- self,
74
- from_token_address: str,
75
- to_token_address: str,
76
- from_chain_id: int,
77
- to_chain_id: int,
78
- from_address: str,
79
- to_address: str,
80
- amount: str,
81
- slippage: float | None = None,
82
- wayfinder_fee: float | None = None,
83
- preferred_providers: list[str] | None = None,
84
- ) -> tuple[bool, dict[str, Any] | str]:
85
- try:
86
- data = await self.brap_client.get_quote(
87
- from_token=from_token_address,
88
- to_token=to_token_address,
89
- from_chain=from_chain_id,
90
- to_chain=to_chain_id,
91
- from_wallet=from_address,
92
- from_amount=amount,
93
- )
94
-
95
- raw_quotes = data.get("quotes")
96
- if isinstance(raw_quotes, list):
97
- all_quotes = raw_quotes
98
- best_quote = data.get("best_quote") or data.get("best_route")
99
- else:
100
- quotes_container = raw_quotes or {}
101
- all_quotes = quotes_container.get(
102
- "all_quotes", []
103
- ) or quotes_container.get("quotes", [])
104
- best_quote = (
105
- quotes_container.get("best_quote")
106
- or data.get("best_quote")
107
- or data.get("best_route")
108
- )
109
-
110
- # If preferred providers specified, select by provider preference
111
- if preferred_providers and all_quotes:
112
- selected_quote = self._select_quote_by_provider(
113
- all_quotes, preferred_providers
114
- )
115
- if selected_quote:
116
- return (True, selected_quote)
117
- # Fall through to best_quote if no preferred provider found
118
-
119
- if not best_quote:
120
- return (False, "No quotes available")
121
-
122
- return (True, best_quote)
123
- except Exception as e:
124
- self.logger.error(f"Error getting best quote: {e}")
125
- return (False, str(e))
126
-
127
38
  def _select_quote_by_provider(
128
39
  self,
129
40
  quotes: list[dict[str, Any]],
130
41
  preferred_providers: list[str],
131
42
  ) -> dict[str, Any] | None:
132
- # Normalize preferred providers to lowercase for case-insensitive matching
133
- preferred_lower = [p.lower() for p in preferred_providers]
134
-
135
- provider_quotes: dict[str, list[dict[str, Any]]] = {}
136
- for quote in quotes:
137
- # Provider name might be in different fields depending on BRAP response structure
138
- provider = (
139
- quote.get("provider")
140
- or quote.get("provider_name")
141
- or quote.get("source")
142
- or quote.get("protocol")
143
- or ""
144
- ).lower()
145
- if provider:
146
- if provider not in provider_quotes:
147
- provider_quotes[provider] = []
148
- provider_quotes[provider].append(quote)
149
-
150
- # Select first matching provider in preference order
151
- for pref in preferred_lower:
152
- if pref in provider_quotes:
153
- provider_list = provider_quotes[pref]
154
- best_for_provider = max(
155
- provider_list, key=lambda q: int(q.get("output_amount", 0) or 0)
156
- )
157
- self.logger.info(f"Selected quote from preferred provider: {pref}")
158
- return best_for_provider
159
-
160
- # Log available providers for debugging
161
- available = list(provider_quotes.keys())
162
- self.logger.warning(
163
- f"No preferred provider found. Wanted: {preferred_providers}, Available: {available}"
164
- )
43
+ preferred_lower = {p.lower() for p in preferred_providers}
44
+ matching = [
45
+ q for q in quotes if q.get("provider", "").lower() in preferred_lower
46
+ ]
47
+ if matching:
48
+ return max(matching, key=lambda q: int(q.get("output_amount", 0) or 0))
165
49
  return None
166
50
 
167
- async def calculate_swap_fees(
51
+ async def _handle_token_approval(
168
52
  self,
169
- from_token_address: str,
170
- to_token_address: str,
171
- from_chain_id: int,
172
- to_chain_id: int,
173
- amount: str,
174
- slippage: float | None = None,
175
- ) -> tuple[bool, Any]:
176
- try:
177
- success, quote_data = await self.get_swap_quote(
178
- from_token_address=from_token_address,
179
- to_token_address=to_token_address,
180
- from_chain_id=from_chain_id,
181
- to_chain_id=to_chain_id,
182
- from_address="0x0000000000000000000000000000000000000000",
183
- to_address="0x0000000000000000000000000000000000000000",
184
- amount=amount,
185
- slippage=slippage,
53
+ *,
54
+ chain_id: int,
55
+ token_address: str,
56
+ owner_address: str,
57
+ spender_address: str,
58
+ amount: int,
59
+ ) -> None:
60
+ token_checksum = Web3.to_checksum_address(token_address)
61
+ owner_checksum = Web3.to_checksum_address(owner_address)
62
+ spender_checksum = Web3.to_checksum_address(spender_address)
63
+
64
+ if (chain_id, token_checksum.lower()) in TOKENS_REQUIRING_APPROVAL_RESET:
65
+ allowance = await get_token_allowance(
66
+ token_checksum,
67
+ chain_id,
68
+ owner_checksum,
69
+ spender_checksum,
186
70
  )
71
+ if allowance > 0:
72
+ clear_tx = await build_approve_transaction(
73
+ from_address=owner_checksum,
74
+ chain_id=chain_id,
75
+ token_address=token_checksum,
76
+ spender_address=spender_checksum,
77
+ amount=0,
78
+ )
79
+ await send_transaction(clear_tx, self.strategy_wallet_signing_callback)
187
80
 
188
- if not success:
189
- return (False, quote_data)
81
+ approve_tx = await build_approve_transaction(
82
+ from_address=owner_checksum,
83
+ chain_id=chain_id,
84
+ token_address=token_checksum,
85
+ spender_address=spender_checksum,
86
+ amount=int(amount),
87
+ )
88
+ await send_transaction(approve_tx, self.strategy_wallet_signing_callback)
190
89
 
191
- best_quote = quote_data.get("best_quote")
90
+ async def _record_swap_operation(
91
+ self,
92
+ from_token: dict[str, Any],
93
+ to_token: dict[str, Any],
94
+ wallet_address: str,
95
+ quote: dict[str, Any],
96
+ tx_hash: str,
97
+ strategy_name: str | None = None,
98
+ ) -> TransactionRecord | dict[str, Any]:
99
+ from_amount_usd = quote.get("from_amount_usd", 0)
100
+ to_amount_usd = quote.get("to_amount_usd", 0)
101
+ if from_amount_usd is None:
102
+ from_amount_usd = await self.token_adapter.get_amount_usd(
103
+ from_token.get("token_id"),
104
+ quote.get("input_amount"),
105
+ from_token.get("decimals") or 18,
106
+ )
107
+ if to_amount_usd is None:
108
+ to_amount_usd = await self.token_adapter.get_amount_usd(
109
+ to_token.get("token_id"),
110
+ quote.get("output_amount"),
111
+ to_token.get("decimals") or 18,
112
+ )
192
113
 
193
- if not best_quote:
194
- return (False, "No quote available for fee calculation")
114
+ operation_data = SWAP(
115
+ adapter=self.adapter_type,
116
+ from_token_id=str(from_token.get("id")),
117
+ to_token_id=str(to_token.get("id")),
118
+ from_amount=str(quote.get("input_amount")),
119
+ to_amount=str(quote.get("output_amount")),
120
+ from_amount_usd=from_amount_usd,
121
+ to_amount_usd=to_amount_usd,
122
+ transaction_hash=tx_hash,
123
+ transaction_chain_id=from_token.get("chain_id")
124
+ or (from_token.get("chain") or {}).get("id"),
125
+ transaction_status=None,
126
+ transaction_receipt=None,
127
+ )
195
128
 
196
- fee_estimate = best_quote.get("fee_estimate", {})
197
- fees = {
198
- "input_amount": best_quote.get("input_amount", 0),
199
- "output_amount": best_quote.get("output_amount", 0),
200
- "gas_fee": best_quote.get("gas_estimate") or 0,
201
- "bridge_fee": 0,
202
- "protocol_fee": fee_estimate.get("fee_total_usd", 0),
203
- "total_fee": fee_estimate.get("fee_total_usd", 0),
204
- "slippage": 0,
205
- "price_impact": best_quote.get("quote", {}).get("priceImpact", 0),
206
- }
129
+ try:
130
+ success, ledger_response = await self.ledger_adapter.record_operation(
131
+ wallet_address=wallet_address,
132
+ operation_data=operation_data,
133
+ usd_value=from_amount_usd or 0,
134
+ strategy_name=strategy_name,
135
+ )
136
+ if success:
137
+ return ledger_response
138
+ self.logger.warning(
139
+ "Ledger swap record failed", error=ledger_response, quote=quote
140
+ )
141
+ except Exception as exc:
142
+ self.logger.warning(f"Ledger swap record raised: {exc}", quote=quote)
207
143
 
208
- return (True, fees)
209
- except Exception as e:
210
- self.logger.error(f"Error calculating swap fees: {e}")
211
- return (False, str(e))
144
+ return operation_data.model_dump(mode="json")
212
145
 
213
- async def compare_routes(
146
+ async def best_quote(
214
147
  self,
215
148
  from_token_address: str,
216
149
  to_token_address: str,
217
150
  from_chain_id: int,
218
151
  to_chain_id: int,
152
+ from_address: str,
219
153
  amount: str,
154
+ preferred_providers: list[str] | None = None,
155
+ retries: int = 1,
220
156
  slippage: float | None = None,
221
- ) -> tuple[bool, Any]:
222
- try:
223
- data = await self.brap_client.get_quote(
224
- from_token=from_token_address,
225
- to_token=to_token_address,
226
- from_chain=from_chain_id,
227
- to_chain=to_chain_id,
228
- from_wallet="0x0000000000000000000000000000000000000000",
229
- from_amount=amount,
230
- )
231
-
232
- raw_quotes = data.get("quotes")
233
- if isinstance(raw_quotes, list):
234
- all_quotes = raw_quotes
235
- best_quote = data.get("best_quote") or data.get("best_route")
236
- else:
237
- quotes = raw_quotes or {}
238
- all_quotes = quotes.get("all_quotes", []) or quotes.get("quotes", [])
239
- best_quote = (
240
- quotes.get("best_quote")
241
- or data.get("best_quote")
242
- or data.get("best_route")
157
+ ) -> tuple[bool, dict[str, Any] | str]:
158
+ last_error = "No quotes available"
159
+ for attempt in range(retries):
160
+ try:
161
+ data = await self.brap_client.get_quote(
162
+ from_token=from_token_address,
163
+ to_token=to_token_address,
164
+ from_chain=from_chain_id,
165
+ to_chain=to_chain_id,
166
+ from_wallet=from_address,
167
+ from_amount=amount,
168
+ slippage=slippage,
243
169
  )
244
170
 
245
- if not all_quotes:
246
- return (False, "No routes available")
171
+ all_quotes, quote = data.get("quotes", []), data.get("best_quote")
247
172
 
248
- # Sort quotes by output amount (descending)
249
- sorted_quotes = sorted(
250
- all_quotes, key=lambda x: int(x.get("output_amount", 0)), reverse=True
251
- )
173
+ if preferred_providers and all_quotes:
174
+ selected = self._select_quote_by_provider(
175
+ all_quotes, preferred_providers
176
+ )
177
+ if selected:
178
+ return (True, selected)
252
179
 
253
- comparison = {
254
- "total_routes": len(all_quotes),
255
- "best_route": best_quote,
256
- "all_routes": sorted_quotes,
257
- "route_analysis": {
258
- "highest_output": sorted_quotes[0] if sorted_quotes else None,
259
- "lowest_fees": (
260
- min(
261
- all_quotes,
262
- key=lambda x: float(
263
- x.get("fee_estimate", {}).get("fee_total_usd", 0)
264
- ),
265
- )
266
- if all_quotes
267
- else None
268
- ),
269
- "fastest": (
270
- min(all_quotes, key=lambda x: int(x.get("estimated_time", 0)))
271
- if all_quotes
272
- else None
273
- ),
274
- },
275
- }
276
-
277
- return (True, comparison)
278
- except Exception as e:
279
- self.logger.error(f"Error comparing routes: {e}")
280
- return (False, str(e))
180
+ if quote:
181
+ return (True, quote)
281
182
 
282
- async def swap_from_token_ids(
283
- self,
284
- from_token_id: str,
285
- to_token_id: str,
286
- from_address: str,
287
- amount: str,
288
- slippage: float = DEFAULT_SLIPPAGE,
289
- strategy_name: str | None = None,
290
- preferred_providers: list[str] | None = None,
291
- ) -> tuple[bool, Any]:
292
- from_token = await self.token_client.get_token_details(from_token_id)
293
- if not from_token:
294
- return (False, f"From token not found: {from_token_id}")
295
- to_token = await self.token_client.get_token_details(to_token_id)
296
- if not to_token:
297
- return (False, f"To token not found: {to_token_id}")
183
+ last_error = "No quotes available"
184
+ except Exception as e:
185
+ last_error = str(e)
186
+ if attempt < retries - 1:
187
+ self.logger.warning(
188
+ f"Quote attempt {attempt + 1}/{retries} failed: {e}"
189
+ )
298
190
 
299
- success, best_quote = await self.get_best_quote(
300
- from_token_address=from_token.get("address"),
301
- to_token_address=to_token.get("address"),
302
- from_chain_id=(from_token.get("chain") or {}).get("id"),
303
- to_chain_id=(to_token.get("chain") or {}).get("id"),
304
- from_address=from_address,
305
- to_address=from_address,
306
- amount=amount,
307
- slippage=slippage,
308
- preferred_providers=preferred_providers,
309
- )
310
- if not success:
311
- return (False, best_quote)
312
-
313
- return await self.swap_from_quote(
314
- from_token=from_token,
315
- to_token=to_token,
316
- from_address=from_address,
317
- quote=best_quote,
318
- strategy_name=strategy_name,
319
- )
191
+ self.logger.error(f"All {retries} quote attempts failed: {last_error}")
192
+ return (False, last_error)
320
193
 
321
194
  async def swap_from_quote(
322
195
  self,
@@ -326,33 +199,19 @@ class BRAPAdapter(BaseAdapter):
326
199
  quote: dict[str, Any],
327
200
  strategy_name: str | None = None,
328
201
  ) -> tuple[bool, Any]:
329
- chain = from_token.get("chain") or {}
330
- chain_id = self._chain_id(chain)
202
+ chain_id = from_token["chain"]["id"]
331
203
 
332
- calldata = quote.get("calldata") or {}
333
- transaction = dict(calldata)
334
- if not transaction or not transaction.get("data"):
204
+ calldata = quote.get("calldata")
205
+ if not calldata or not calldata.get("data"):
335
206
  return (False, "Quote missing calldata")
336
- transaction["chainId"] = chain_id
337
- if "value" in transaction:
338
- transaction["value"] = int(transaction["value"])
339
- # Always set the sender to the strategy wallet for broadcast.
340
- # (Calldata may include either "from" or "from_address" depending on provider.)
341
- transaction["from"] = to_checksum_address(from_address)
342
-
343
- def _as_address(value: Any) -> str | None:
344
- if not isinstance(value, str):
345
- return None
346
- v = value.strip()
347
- if (
348
- v.startswith("0x")
349
- and len(v) == 42
350
- and v.lower() != ZERO_ADDRESS.lower()
351
- ):
352
- return v
353
- return None
354
207
 
355
- spender = transaction.get("to")
208
+ transaction = {
209
+ **calldata,
210
+ "chainId": chain_id,
211
+ "from": Web3.to_checksum_address(from_address),
212
+ }
213
+ if "value" in calldata:
214
+ transaction["value"] = int(calldata["value"])
356
215
 
357
216
  approve_amount = (
358
217
  quote.get("input_amount")
@@ -360,66 +219,34 @@ class BRAPAdapter(BaseAdapter):
360
219
  or transaction.get("value")
361
220
  )
362
221
  token_address = from_token.get("address")
363
- token_address_l = str(token_address or "").lower()
364
- is_native = token_address_l in {
365
- "",
366
- ZERO_ADDRESS.lower(),
367
- "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
368
- }
369
- if token_address and (not is_native) and spender and approve_amount:
370
- approve_success, approve_response = await self._handle_token_approval(
371
- chain=chain,
222
+
223
+ spender = transaction.get("to")
224
+ if (
225
+ token_address
226
+ and spender
227
+ and approve_amount
228
+ and not is_native_token(token_address)
229
+ ):
230
+ await self._handle_token_approval(
231
+ chain_id=chain_id,
372
232
  token_address=from_token.get("address"),
373
233
  owner_address=from_address,
374
234
  spender_address=spender,
375
235
  amount=int(approve_amount),
376
236
  )
377
- if not approve_success:
378
- return (False, approve_response)
379
237
 
380
- broadcast_success, broadcast_response = await self._send_tx(transaction)
381
- self.logger.info(
382
- f"Swap broadcast result: success={broadcast_success}, "
383
- f"response={broadcast_response}"
238
+ txn_hash = await send_transaction(
239
+ transaction, self.strategy_wallet_signing_callback
384
240
  )
385
- # Log only key fields to avoid spamming raw HexBytes logs
386
- if isinstance(broadcast_response, dict):
387
- tx_hash_log = broadcast_response.get("tx_hash", "unknown")
388
- block_log = broadcast_response.get("block_number", "unknown")
389
- status_log = (
390
- broadcast_response.get("receipt", {}).get("status", "unknown")
391
- if isinstance(broadcast_response.get("receipt"), dict)
392
- else "unknown"
393
- )
394
- self.logger.info(
395
- f"Swap broadcast: success={broadcast_success}, tx={tx_hash_log}, block={block_log}, status={status_log}"
396
- )
397
- else:
398
- self.logger.info(f"Swap broadcast: success={broadcast_success}")
399
- if not broadcast_success:
400
- return (False, broadcast_response)
401
-
402
- tx_hash = None
403
- block_number = None
404
- confirmations = None
405
- confirmed_block_number = None
406
- if isinstance(broadcast_response, dict):
407
- tx_hash = broadcast_response.get("tx_hash") or broadcast_response.get(
408
- "transaction_hash"
409
- )
410
- block_number = broadcast_response.get("block_number")
411
- confirmations = broadcast_response.get("confirmations")
412
- confirmed_block_number = broadcast_response.get("confirmed_block_number")
241
+ self.logger.info(f"Swap broadcast: tx={txn_hash}")
413
242
 
414
- # Record the swap operation in ledger - but don't let ledger errors fail the swap
415
- # since the on-chain transaction already succeeded
416
243
  try:
417
244
  ledger_record = await self._record_swap_operation(
418
245
  from_token=from_token,
419
246
  to_token=to_token,
420
247
  wallet_address=from_address,
421
248
  quote=quote,
422
- broadcast_response=broadcast_response,
249
+ tx_hash=txn_hash,
423
250
  strategy_name=strategy_name,
424
251
  )
425
252
  except Exception as e:
@@ -431,264 +258,48 @@ class BRAPAdapter(BaseAdapter):
431
258
  result_payload: dict[str, Any] = {
432
259
  "from_amount": quote.get("input_amount"),
433
260
  "to_amount": quote.get("output_amount"),
434
- "tx_hash": tx_hash,
435
- "block_number": block_number,
436
- "confirmations": confirmations,
437
- "confirmed_block_number": confirmed_block_number,
261
+ "tx_hash": txn_hash,
262
+ "ledger_record": ledger_record,
438
263
  }
439
- if isinstance(ledger_record, dict):
440
- result_payload.update(ledger_record)
441
264
 
442
265
  return (True, result_payload)
443
266
 
444
- async def get_bridge_quote(
267
+ async def swap_from_token_ids(
445
268
  self,
446
- from_token_address: str,
447
- to_token_address: str,
448
- from_chain_id: int,
449
- to_chain_id: int,
269
+ from_token_id: str,
270
+ to_token_id: str,
271
+ from_address: str,
450
272
  amount: str,
273
+ strategy_name: str | None = None,
274
+ preferred_providers: list[str] | None = None,
275
+ retries: int = 1,
451
276
  slippage: float | None = None,
452
277
  ) -> tuple[bool, Any]:
453
- # For BRAP, bridge operations are the same as swap operations
454
- return await self.get_swap_quote(
455
- from_token_address=from_token_address,
456
- to_token_address=to_token_address,
457
- from_chain_id=from_chain_id,
458
- to_chain_id=to_chain_id,
459
- from_address="0x0000000000000000000000000000000000000000",
460
- to_address="0x0000000000000000000000000000000000000000",
278
+ from_token = await self.token_client.get_token_details(from_token_id)
279
+ to_token = await self.token_client.get_token_details(to_token_id)
280
+ if not from_token:
281
+ return (False, f"From token not found: {from_token_id}")
282
+ if not to_token:
283
+ return (False, f"To token not found: {to_token_id}")
284
+
285
+ success, quote = await self.best_quote(
286
+ from_token_address=from_token.get("address"),
287
+ to_token_address=to_token.get("address"),
288
+ from_chain_id=(from_token.get("chain") or {}).get("id"),
289
+ to_chain_id=(to_token.get("chain") or {}).get("id"),
290
+ from_address=from_address,
461
291
  amount=amount,
292
+ preferred_providers=preferred_providers,
293
+ retries=retries,
462
294
  slippage=slippage,
463
295
  )
296
+ if not success:
297
+ return (False, quote)
464
298
 
465
- async def estimate_gas_cost(
466
- self, from_chain_id: int, to_chain_id: int, operation_type: str = "swap"
467
- ) -> tuple[bool, Any]:
468
- try:
469
- # This is a simplified estimation - in practice, you'd want to
470
- # query actual gas prices from the chains
471
- gas_estimates = {
472
- "ethereum": {"swap": 150000, "bridge": 200000},
473
- "base": {"swap": 100000, "bridge": 150000},
474
- "arbitrum": {"swap": 80000, "bridge": 120000},
475
- "polygon": {"swap": 60000, "bridge": 100000},
476
- }
477
-
478
- # Map chain IDs to names (simplified)
479
- chain_names = {
480
- 1: "ethereum",
481
- 8453: "base",
482
- 42161: "arbitrum",
483
- 137: "polygon",
484
- }
485
-
486
- from_chain = chain_names.get(from_chain_id, "unknown")
487
- to_chain = chain_names.get(to_chain_id, "unknown")
488
-
489
- from_gas = gas_estimates.get(from_chain, {}).get(operation_type, 100000)
490
- to_gas = gas_estimates.get(to_chain, {}).get(operation_type, 100000)
491
-
492
- return (
493
- True,
494
- {
495
- "from_chain": from_chain,
496
- "to_chain": to_chain,
497
- "from_gas_estimate": from_gas,
498
- "to_gas_estimate": to_gas,
499
- "total_operations": 2 if from_chain_id != to_chain_id else 1,
500
- "operation_type": operation_type,
501
- },
502
- )
503
- except Exception as e:
504
- self.logger.error(f"Error estimating gas cost: {e}")
505
- return (False, str(e))
506
-
507
- async def validate_swap_parameters(
508
- self,
509
- from_token_address: str,
510
- to_token_address: str,
511
- from_chain_id: int,
512
- to_chain_id: int,
513
- amount: str,
514
- ) -> tuple[bool, Any]:
515
- try:
516
- validation_errors = []
517
-
518
- # Basic validation
519
- if not from_token_address or len(from_token_address) != 42:
520
- validation_errors.append("Invalid from_token_address")
521
-
522
- if not to_token_address or len(to_token_address) != 42:
523
- validation_errors.append("Invalid to_token_address")
524
-
525
- if from_chain_id <= 0 or to_chain_id <= 0:
526
- validation_errors.append("Invalid chain IDs")
527
-
528
- try:
529
- amount_int = int(amount)
530
- if amount_int <= 0:
531
- validation_errors.append("Amount must be positive")
532
- except (ValueError, TypeError):
533
- validation_errors.append("Invalid amount format")
534
-
535
- if validation_errors:
536
- return (False, {"valid": False, "errors": validation_errors})
537
-
538
- # Try to get a quote to validate the swap is possible
539
- success, quote_data = await self.get_swap_quote(
540
- from_token_address=from_token_address,
541
- to_token_address=to_token_address,
542
- from_chain_id=from_chain_id,
543
- to_chain_id=to_chain_id,
544
- from_address="0x0000000000000000000000000000000000000000",
545
- to_address="0x0000000000000000000000000000000000000000",
546
- amount=amount,
547
- )
548
-
549
- if not success:
550
- validation_errors.append(f"Swap not possible: {quote_data}")
551
- return (False, {"valid": False, "errors": validation_errors})
552
-
553
- best_quote = (
554
- quote_data.get("best_quote", {}) if isinstance(quote_data, dict) else {}
555
- )
556
- return (
557
- True,
558
- {
559
- "valid": True,
560
- "quote_available": True,
561
- "estimated_output": str(best_quote.get("output_amount", 0)),
562
- },
563
- )
564
- except Exception as e:
565
- self.logger.error(f"Error validating swap parameters: {e}")
566
- return (False, str(e))
567
-
568
- async def _handle_token_approval(
569
- self,
570
- *,
571
- chain: dict[str, Any],
572
- token_address: str,
573
- owner_address: str,
574
- spender_address: str,
575
- amount: int,
576
- ) -> tuple[bool, Any]:
577
- chain_id = self._chain_id(chain)
578
- token_checksum = to_checksum_address(token_address)
579
- owner_checksum = to_checksum_address(owner_address)
580
- spender_checksum = to_checksum_address(spender_address)
581
-
582
- if (chain_id, token_checksum.lower()) in _NEEDS_CLEAR_APPROVAL:
583
- allowance = await get_token_allowance(
584
- token_checksum,
585
- chain_id,
586
- owner_checksum,
587
- spender_checksum,
588
- )
589
- if allowance > 0:
590
- clear_tx = await build_approve_transaction(
591
- from_address=owner_checksum,
592
- chain_id=chain_id,
593
- token_address=token_checksum,
594
- spender_address=spender_checksum,
595
- amount=0,
596
- )
597
- clear_result = await self._send_tx(clear_tx)
598
- if not clear_result[0]:
599
- return clear_result
600
-
601
- approve_tx = await build_approve_transaction(
602
- from_address=owner_checksum,
603
- chain_id=chain_id,
604
- token_address=token_checksum,
605
- spender_address=spender_checksum,
606
- amount=int(amount),
607
- )
608
- return await self._send_tx(approve_tx)
609
-
610
- async def _send_tx(self, tx: dict[str, Any]) -> tuple[bool, Any]:
611
- txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
612
- return True, txn_hash
613
-
614
- async def _record_swap_operation(
615
- self,
616
- from_token: dict[str, Any],
617
- to_token: dict[str, Any],
618
- wallet_address: str,
619
- quote: dict[str, Any],
620
- broadcast_response: dict[str, Any] | Any,
621
- strategy_name: str | None = None,
622
- ) -> TransactionRecord | dict[str, Any]:
623
- from_amount_usd = quote.get("from_amount_usd")
624
- if from_amount_usd is None:
625
- from_amount_usd = await self._token_amount_usd(
626
- from_token, quote.get("input_amount")
627
- )
628
-
629
- to_amount_usd = quote.get("to_amount_usd")
630
- if to_amount_usd is None:
631
- to_amount_usd = await self._token_amount_usd(
632
- to_token, quote.get("output_amount")
633
- )
634
-
635
- response = broadcast_response if isinstance(broadcast_response, dict) else {}
636
- operation_data = SWAP(
637
- adapter=self.adapter_type,
638
- from_token_id=str(from_token.get("id")),
639
- to_token_id=str(to_token.get("id")),
640
- from_amount=str(quote.get("input_amount")),
641
- to_amount=str(quote.get("output_amount")),
642
- from_amount_usd=from_amount_usd or 0,
643
- to_amount_usd=to_amount_usd or 0,
644
- transaction_hash=response.get("tx_hash")
645
- or response.get("transaction_hash"),
646
- transaction_chain_id=from_token.get("chain_id")
647
- or (from_token.get("chain") or {}).get("id"),
648
- transaction_status=response.get("transaction_status"),
649
- # Don't pass raw receipt - it contains HexBytes that can't be JSON serialized
650
- transaction_receipt=None,
651
- )
652
-
653
- try:
654
- success, ledger_response = await self.ledger_adapter.record_operation(
655
- wallet_address=wallet_address,
656
- operation_data=operation_data,
657
- usd_value=from_amount_usd or 0,
658
- strategy_name=strategy_name,
659
- )
660
- if success:
661
- return ledger_response
662
- self.logger.warning(
663
- "Ledger swap record failed", error=ledger_response, quote=quote
664
- )
665
- except Exception as exc: # noqa: BLE001
666
- self.logger.warning(f"Ledger swap record raised: {exc}", quote=quote)
667
-
668
- return operation_data.model_dump(mode="json")
669
-
670
- async def _token_amount_usd(
671
- self, token_info: dict[str, Any], raw_amount: Any
672
- ) -> float | None:
673
- if raw_amount is None:
674
- return None
675
- success, price_data = await self.token_adapter.get_token_price(
676
- token_info.get("token_id")
677
- )
678
- if not success or not price_data:
679
- return None
680
- decimals = token_info.get("decimals") or 18
681
- return (
682
- price_data.get("current_price", 0.0)
683
- * float(raw_amount)
684
- / 10 ** int(decimals)
299
+ return await self.swap_from_quote(
300
+ from_token=from_token,
301
+ to_token=to_token,
302
+ from_address=from_address,
303
+ quote=quote,
304
+ strategy_name=strategy_name,
685
305
  )
686
-
687
- def _chain_id(self, chain: Any) -> int:
688
- if isinstance(chain, dict):
689
- chain_id = chain.get("id")
690
- else:
691
- chain_id = getattr(chain, "id", None)
692
- if chain_id is None:
693
- raise ValueError("Chain ID is required")
694
- return int(chain_id)