wayfinder-paths 0.1.22__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.
- wayfinder_paths/__init__.py +0 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +65 -169
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -113
- wayfinder_paths/adapters/brap_adapter/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +39 -86
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +5 -1
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +6 -5
- wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +108 -198
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +37 -23
- wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
- wayfinder_paths/core/__init__.py +0 -3
- wayfinder_paths/core/clients/BRAPClient.py +3 -0
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/WayfinderClient.py +0 -1
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +0 -13
- wayfinder_paths/core/config.py +0 -164
- wayfinder_paths/core/constants/__init__.py +58 -2
- wayfinder_paths/core/constants/base.py +8 -22
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +39 -0
- wayfinder_paths/core/constants/tokens.py +9 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -10
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +13 -7
- wayfinder_paths/core/utils/web3.py +5 -3
- wayfinder_paths/policies/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +19 -14
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +12 -11
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +20 -33
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +21 -18
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -130
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +32 -42
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.23.dist-info}/METADATA +2 -3
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.23.dist-info}/RECORD +51 -60
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.23.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/templates/strategy/test_strategy.py +0 -166
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- {wayfinder_paths-0.1.22.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
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
51
|
+
async def _handle_token_approval(
|
|
168
52
|
self,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
) ->
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
246
|
-
return (False, "No routes available")
|
|
171
|
+
all_quotes, quote = data.get("quotes", []), data.get("best_quote")
|
|
247
172
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
330
|
-
chain_id = self._chain_id(chain)
|
|
202
|
+
chain_id = from_token["chain"]["id"]
|
|
331
203
|
|
|
332
|
-
calldata = quote.get("calldata")
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
435
|
-
"
|
|
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
|
|
267
|
+
async def swap_from_token_ids(
|
|
445
268
|
self,
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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)
|