wayfinder-paths 0.1.13__py3-none-any.whl → 0.1.14__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/adapters/balance_adapter/README.md +13 -14
- wayfinder_paths/adapters/balance_adapter/adapter.py +33 -32
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
- wayfinder_paths/adapters/brap_adapter/README.md +11 -16
- wayfinder_paths/adapters/brap_adapter/adapter.py +78 -63
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +121 -59
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +16 -14
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +114 -60
- wayfinder_paths/adapters/pool_adapter/README.md +9 -10
- wayfinder_paths/adapters/pool_adapter/adapter.py +9 -10
- wayfinder_paths/adapters/token_adapter/README.md +2 -14
- wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
- wayfinder_paths/adapters/token_adapter/examples.json +4 -8
- wayfinder_paths/adapters/token_adapter/test_adapter.py +5 -3
- wayfinder_paths/core/clients/BRAPClient.py +102 -61
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +125 -64
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +122 -48
- wayfinder_paths/core/clients/TokenClient.py +91 -36
- wayfinder_paths/core/clients/WalletClient.py +26 -56
- wayfinder_paths/core/clients/WayfinderClient.py +28 -160
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -46
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/engine/StrategyJob.py +7 -55
- wayfinder_paths/core/services/local_evm_txn.py +6 -6
- wayfinder_paths/core/services/local_token_txn.py +1 -1
- wayfinder_paths/core/strategies/Strategy.py +0 -2
- wayfinder_paths/core/utils/evm_helpers.py +2 -2
- wayfinder_paths/run_strategy.py +8 -19
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +10 -11
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +40 -25
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +3 -3
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +12 -6
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +1 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +88 -56
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +16 -12
- wayfinder_paths/templates/strategy/README.md +3 -3
- wayfinder_paths/templates/strategy/test_strategy.py +3 -2
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.14.dist-info}/METADATA +14 -49
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.14.dist-info}/RECORD +46 -47
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.14.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.14.dist-info}/WHEEL +0 -0
|
@@ -23,24 +23,23 @@ balance = BalanceAdapter(config, web3_service=web3_service)
|
|
|
23
23
|
|
|
24
24
|
## API surface
|
|
25
25
|
|
|
26
|
-
### `get_balance(
|
|
27
|
-
Returns the raw balance (as an integer) for a specific token on a wallet.
|
|
26
|
+
### `get_balance(*, query: str | dict, wallet_address: str, chain_id: int | None = None)`
|
|
27
|
+
Returns the raw balance (as an integer) for a specific token or pool on a wallet.
|
|
28
|
+
|
|
29
|
+
`query`: token_id/address string or a dict with a `"token_id"` key. When `query` is a token identifier (e.g. `"usd-coin-base"`), `chain_id` is auto-resolved from token info; when it is a pool address, `chain_id` must be provided.
|
|
28
30
|
|
|
29
31
|
```python
|
|
32
|
+
# Token balance (chain_id auto-resolved)
|
|
30
33
|
success, balance = await balance.get_balance(
|
|
31
|
-
|
|
34
|
+
query="usd-coin-base",
|
|
32
35
|
wallet_address=config["main_wallet"]["address"],
|
|
33
36
|
)
|
|
34
|
-
```
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
success, amount = await balance.get_pool_balance(
|
|
41
|
-
pool_address="0xPool",
|
|
38
|
+
# Pool balance (chain_id required)
|
|
39
|
+
success, pool_balance = await balance.get_balance(
|
|
40
|
+
query="0xPool...",
|
|
41
|
+
wallet_address=config["strategy_wallet"]["address"],
|
|
42
42
|
chain_id=8453,
|
|
43
|
-
user_address=config["strategy_wallet"]["address"],
|
|
44
43
|
)
|
|
45
44
|
```
|
|
46
45
|
|
|
@@ -80,10 +79,10 @@ class MyStrategy(Strategy):
|
|
|
80
79
|
self.balance_adapter = balance_adapter
|
|
81
80
|
|
|
82
81
|
async def _status(self):
|
|
83
|
-
success, pool_balance = await self.balance_adapter.
|
|
84
|
-
|
|
82
|
+
success, pool_balance = await self.balance_adapter.get_balance(
|
|
83
|
+
query=self.current_pool["address"],
|
|
84
|
+
wallet_address=self.config["strategy_wallet"]["address"],
|
|
85
85
|
chain_id=self.current_pool["chain"]["id"],
|
|
86
|
-
user_address=self.config["strategy_wallet"]["address"],
|
|
87
86
|
)
|
|
88
87
|
return {"portfolio_value": float(pool_balance or 0), ...}
|
|
89
88
|
```
|
|
@@ -42,16 +42,39 @@ class BalanceAdapter(BaseAdapter):
|
|
|
42
42
|
async def get_balance(
|
|
43
43
|
self,
|
|
44
44
|
*,
|
|
45
|
-
|
|
45
|
+
query: str | dict[str, Any],
|
|
46
46
|
wallet_address: str,
|
|
47
|
+
chain_id: int | None = None,
|
|
47
48
|
) -> tuple[bool, str | int]:
|
|
48
|
-
"""Get token balance for a wallet.
|
|
49
|
+
"""Get token or pool balance for a wallet.
|
|
50
|
+
|
|
51
|
+
query: token_id/address string or a dict with a "token_id" key.
|
|
52
|
+
"""
|
|
53
|
+
resolved = query if isinstance(query, str) else (query or {}).get("token_id")
|
|
54
|
+
if not resolved:
|
|
55
|
+
return (False, "missing query")
|
|
49
56
|
try:
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
if chain_id is None:
|
|
58
|
+
token_info = await self.token_client.get_token_details(resolved)
|
|
59
|
+
if not token_info:
|
|
60
|
+
return (False, f"Token not found: {resolved}")
|
|
61
|
+
resolved_chain_id = resolve_chain_id(token_info, self.logger)
|
|
62
|
+
if resolved_chain_id is None:
|
|
63
|
+
return (False, f"Token {resolved} is missing a chain id")
|
|
64
|
+
chain_id = resolved_chain_id
|
|
65
|
+
|
|
66
|
+
data = await self.wallet_client.get_token_balance_for_address(
|
|
52
67
|
wallet_address=wallet_address,
|
|
68
|
+
query=resolved,
|
|
69
|
+
chain_id=int(chain_id),
|
|
53
70
|
)
|
|
54
|
-
|
|
71
|
+
# Use _parse_balance for consistent parsing (handles balance_raw or balance)
|
|
72
|
+
raw = (
|
|
73
|
+
data.get("balance_raw") or data.get("balance")
|
|
74
|
+
if isinstance(data, dict)
|
|
75
|
+
else None
|
|
76
|
+
)
|
|
77
|
+
return (True, self._parse_balance(raw))
|
|
55
78
|
except Exception as e:
|
|
56
79
|
return (False, str(e))
|
|
57
80
|
|
|
@@ -159,6 +182,7 @@ class BalanceAdapter(BaseAdapter):
|
|
|
159
182
|
|
|
160
183
|
usd_value = await self._token_amount_usd(token_info, amount)
|
|
161
184
|
try:
|
|
185
|
+
token_id = token_info.get("token_id") or token_info.get("id")
|
|
162
186
|
success, response = await ledger_method(
|
|
163
187
|
wallet_address=wallet_address,
|
|
164
188
|
chain_id=chain_id,
|
|
@@ -166,7 +190,7 @@ class BalanceAdapter(BaseAdapter):
|
|
|
166
190
|
token_amount=str(amount),
|
|
167
191
|
usd_value=usd_value,
|
|
168
192
|
data={
|
|
169
|
-
"token_id":
|
|
193
|
+
"token_id": token_id,
|
|
170
194
|
"amount": str(amount),
|
|
171
195
|
"usd_value": usd_value,
|
|
172
196
|
},
|
|
@@ -176,15 +200,16 @@ class BalanceAdapter(BaseAdapter):
|
|
|
176
200
|
self.logger.warning(
|
|
177
201
|
"Ledger entry failed",
|
|
178
202
|
wallet=wallet_address,
|
|
179
|
-
token_id=
|
|
203
|
+
token_id=token_id,
|
|
180
204
|
amount=amount,
|
|
181
205
|
error=response,
|
|
182
206
|
)
|
|
183
207
|
except Exception as exc: # noqa: BLE001
|
|
208
|
+
token_id = token_info.get("token_id") or token_info.get("id")
|
|
184
209
|
self.logger.warning(
|
|
185
210
|
f"Ledger entry raised: {exc}",
|
|
186
211
|
wallet=wallet_address,
|
|
187
|
-
token_id=
|
|
212
|
+
token_id=token_id,
|
|
188
213
|
)
|
|
189
214
|
|
|
190
215
|
async def _token_amount_usd(
|
|
@@ -208,27 +233,3 @@ class BalanceAdapter(BaseAdapter):
|
|
|
208
233
|
if isinstance(evm_wallet, dict):
|
|
209
234
|
return evm_wallet.get("address")
|
|
210
235
|
return None
|
|
211
|
-
|
|
212
|
-
async def get_pool_balance(
|
|
213
|
-
self,
|
|
214
|
-
*,
|
|
215
|
-
pool_address: str,
|
|
216
|
-
chain_id: int,
|
|
217
|
-
user_address: str,
|
|
218
|
-
) -> tuple[bool, Any]:
|
|
219
|
-
"""Get pool balance for a wallet."""
|
|
220
|
-
try:
|
|
221
|
-
data = await self.wallet_client.get_pool_balance_for_wallet(
|
|
222
|
-
pool_address=pool_address,
|
|
223
|
-
chain_id=chain_id,
|
|
224
|
-
user_address=user_address,
|
|
225
|
-
human_readable=False,
|
|
226
|
-
)
|
|
227
|
-
raw = (
|
|
228
|
-
data.get("balance_raw") or data.get("balance")
|
|
229
|
-
if isinstance(data, dict)
|
|
230
|
-
else None
|
|
231
|
-
)
|
|
232
|
-
return (True, self._parse_balance(raw))
|
|
233
|
-
except Exception as e:
|
|
234
|
-
return (False, str(e))
|
|
@@ -57,3 +57,126 @@ class TestBalanceAdapter:
|
|
|
57
57
|
def test_adapter_type(self, adapter):
|
|
58
58
|
"""Test adapter has adapter_type"""
|
|
59
59
|
assert adapter.adapter_type == "BALANCE"
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_get_balance_with_query_string(
|
|
63
|
+
self, adapter, mock_token_client, mock_wallet_client
|
|
64
|
+
):
|
|
65
|
+
"""Test get_balance with query as string (auto-resolves chain_id)."""
|
|
66
|
+
mock_token_client.get_token_details = AsyncMock(
|
|
67
|
+
return_value={
|
|
68
|
+
"token_id": "usd-coin-base",
|
|
69
|
+
"address": "0x123",
|
|
70
|
+
"chain": {"id": 8453, "code": "base"},
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
mock_wallet_client.get_token_balance_for_address = AsyncMock(
|
|
74
|
+
return_value={"balance": 1000000}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
success, balance = await adapter.get_balance(
|
|
78
|
+
query="usd-coin-base",
|
|
79
|
+
wallet_address="0xWallet",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert success is True
|
|
83
|
+
assert balance == 1000000
|
|
84
|
+
mock_token_client.get_token_details.assert_called_once_with("usd-coin-base")
|
|
85
|
+
mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
|
|
86
|
+
wallet_address="0xWallet",
|
|
87
|
+
query="usd-coin-base",
|
|
88
|
+
chain_id=8453,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@pytest.mark.asyncio
|
|
92
|
+
async def test_get_balance_with_query_dict(
|
|
93
|
+
self, adapter, mock_token_client, mock_wallet_client
|
|
94
|
+
):
|
|
95
|
+
"""get_balance accepts query= as dict with token_id key."""
|
|
96
|
+
mock_token_client.get_token_details = AsyncMock(
|
|
97
|
+
return_value={
|
|
98
|
+
"token_id": "wsteth-base",
|
|
99
|
+
"address": "0x456",
|
|
100
|
+
"chain": {"id": 8453, "code": "base"},
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
mock_wallet_client.get_token_balance_for_address = AsyncMock(
|
|
104
|
+
return_value={"balance": 3000000}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
success, balance = await adapter.get_balance(
|
|
108
|
+
query={"token_id": "wsteth-base"},
|
|
109
|
+
wallet_address="0x123",
|
|
110
|
+
)
|
|
111
|
+
assert success is True
|
|
112
|
+
assert balance == 3000000
|
|
113
|
+
mock_token_client.get_token_details.assert_called_once_with("wsteth-base")
|
|
114
|
+
mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
|
|
115
|
+
wallet_address="0x123",
|
|
116
|
+
query="wsteth-base",
|
|
117
|
+
chain_id=8453,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_get_balance_missing_query(self, adapter):
|
|
122
|
+
"""get_balance returns error when query is empty or missing token_id."""
|
|
123
|
+
success, result = await adapter.get_balance(query={}, wallet_address="0xabc")
|
|
124
|
+
assert success is False
|
|
125
|
+
assert "missing query" in str(result)
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_get_balance_with_pool_address(
|
|
129
|
+
self, adapter, mock_token_client, mock_wallet_client
|
|
130
|
+
):
|
|
131
|
+
"""Test get_balance with pool address (explicit chain_id)"""
|
|
132
|
+
mock_wallet_client.get_token_balance_for_address = AsyncMock(
|
|
133
|
+
return_value={"balance": 5000000}
|
|
134
|
+
)
|
|
135
|
+
mock_token_client.get_token_details = AsyncMock()
|
|
136
|
+
|
|
137
|
+
success, balance = await adapter.get_balance(
|
|
138
|
+
query="0xPoolAddress",
|
|
139
|
+
wallet_address="0xWallet",
|
|
140
|
+
chain_id=8453,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
assert success is True
|
|
144
|
+
assert balance == 5000000
|
|
145
|
+
mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
|
|
146
|
+
wallet_address="0xWallet",
|
|
147
|
+
query="0xPoolAddress",
|
|
148
|
+
chain_id=8453,
|
|
149
|
+
)
|
|
150
|
+
mock_token_client.get_token_details.assert_not_called()
|
|
151
|
+
|
|
152
|
+
@pytest.mark.asyncio
|
|
153
|
+
async def test_get_balance_token_not_found(self, adapter, mock_token_client):
|
|
154
|
+
"""Test get_balance when token is not found"""
|
|
155
|
+
mock_token_client.get_token_details = AsyncMock(return_value=None)
|
|
156
|
+
|
|
157
|
+
success, error = await adapter.get_balance(
|
|
158
|
+
query="invalid-token",
|
|
159
|
+
wallet_address="0xWallet",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
assert success is False
|
|
163
|
+
assert "Token not found" in str(error)
|
|
164
|
+
|
|
165
|
+
@pytest.mark.asyncio
|
|
166
|
+
async def test_get_balance_missing_chain_id(self, adapter, mock_token_client):
|
|
167
|
+
"""Test get_balance when chain_id cannot be resolved"""
|
|
168
|
+
mock_token_client.get_token_details = AsyncMock(
|
|
169
|
+
return_value={
|
|
170
|
+
"token_id": "token-without-chain",
|
|
171
|
+
"address": "0x123",
|
|
172
|
+
"chain": {},
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
success, error = await adapter.get_balance(
|
|
177
|
+
query="token-without-chain",
|
|
178
|
+
wallet_address="0xWallet",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
assert success is False
|
|
182
|
+
assert "missing a chain id" in str(error)
|
|
@@ -46,10 +46,9 @@ success, data = await adapter.get_swap_quote(
|
|
|
46
46
|
slippage=0.01 # 1% slippage
|
|
47
47
|
)
|
|
48
48
|
if success:
|
|
49
|
-
|
|
50
|
-
best_quote = quotes.get("best_quote", {})
|
|
49
|
+
best_quote = data.get("best_quote", {})
|
|
51
50
|
print(f"Output amount: {best_quote.get('output_amount')}")
|
|
52
|
-
print(f"
|
|
51
|
+
print(f"Fee estimate: {best_quote.get('fee_estimate', {}).get('fee_total_usd')}")
|
|
53
52
|
else:
|
|
54
53
|
print(f"Error: {data}")
|
|
55
54
|
```
|
|
@@ -68,8 +67,7 @@ success, data = await adapter.get_best_quote(
|
|
|
68
67
|
)
|
|
69
68
|
if success:
|
|
70
69
|
print(f"Best output: {data.get('output_amount')}")
|
|
71
|
-
print(f"
|
|
72
|
-
print(f"Bridge fee: {data.get('bridge_fee')}")
|
|
70
|
+
print(f"Fee estimate (USD): {data.get('fee_estimate', {}).get('fee_total_usd')}")
|
|
73
71
|
else:
|
|
74
72
|
print(f"Error: {data}")
|
|
75
73
|
```
|
|
@@ -88,10 +86,8 @@ success, data = await adapter.calculate_swap_fees(
|
|
|
88
86
|
if success:
|
|
89
87
|
print(f"Input amount: {data.get('input_amount')}")
|
|
90
88
|
print(f"Output amount: {data.get('output_amount')}")
|
|
91
|
-
print(f"Gas
|
|
92
|
-
print(f"
|
|
93
|
-
print(f"Protocol fee: {data.get('protocol_fee')}")
|
|
94
|
-
print(f"Total fee: {data.get('total_fee')}")
|
|
89
|
+
print(f"Gas estimate: {data.get('gas_fee')}")
|
|
90
|
+
print(f"Total fee (USD): {data.get('total_fee')}")
|
|
95
91
|
print(f"Price impact: {data.get('price_impact')}")
|
|
96
92
|
else:
|
|
97
93
|
print(f"Error: {data}")
|
|
@@ -112,7 +108,7 @@ if success:
|
|
|
112
108
|
print(f"Best route output: {data.get('best_route', {}).get('output_amount')}")
|
|
113
109
|
|
|
114
110
|
for i, route in enumerate(data.get('all_routes', [])):
|
|
115
|
-
print(f"Route {i+1}: Output {route.get('output_amount')}, Fee {route.get('
|
|
111
|
+
print(f\"Route {i+1}: Output {route.get('output_amount')}, Fee USD {route.get('fee_estimate', {}).get('fee_total_usd')}\")
|
|
116
112
|
else:
|
|
117
113
|
print(f"Error: {data}")
|
|
118
114
|
```
|
|
@@ -170,7 +166,7 @@ success, data = await adapter.get_bridge_quote(
|
|
|
170
166
|
slippage=0.01
|
|
171
167
|
)
|
|
172
168
|
if success:
|
|
173
|
-
print(f"Bridge quote received: {data.get('
|
|
169
|
+
print(f"Bridge quote received: {data.get('best_quote', {}).get('output_amount')}")
|
|
174
170
|
else:
|
|
175
171
|
print(f"Error: {data}")
|
|
176
172
|
```
|
|
@@ -196,8 +192,7 @@ if success:
|
|
|
196
192
|
fastest = analysis.get("fastest")
|
|
197
193
|
|
|
198
194
|
print(f"Highest output route: {highest_output.get('output_amount')}")
|
|
199
|
-
print(f"Lowest fees route: {lowest_fees.get('
|
|
200
|
-
print(f"Fastest route: {fastest.get('estimated_time')} seconds")
|
|
195
|
+
print(f\"Lowest fees route (USD): {lowest_fees.get('fee_estimate', {}).get('fee_total_usd')}\")
|
|
201
196
|
```
|
|
202
197
|
|
|
203
198
|
### Fee Analysis
|
|
@@ -215,19 +210,19 @@ success, data = await adapter.calculate_swap_fees(
|
|
|
215
210
|
if success:
|
|
216
211
|
input_amount = int(data.get("input_amount", 0))
|
|
217
212
|
output_amount = int(data.get("output_amount", 0))
|
|
218
|
-
|
|
213
|
+
total_fee_usd = float(data.get("total_fee", 0))
|
|
219
214
|
|
|
220
215
|
# Calculate effective rate
|
|
221
216
|
effective_rate = (input_amount - output_amount) / input_amount
|
|
222
217
|
print(f"Effective rate: {effective_rate:.4f} ({effective_rate * 100:.2f}%)")
|
|
223
|
-
print(f"Total fees: {
|
|
218
|
+
print(f"Total fees: ${total_fee_usd:.4f}")
|
|
224
219
|
```
|
|
225
220
|
|
|
226
221
|
## API Endpoints
|
|
227
222
|
|
|
228
223
|
The adapter uses the following Wayfinder API endpoints:
|
|
229
224
|
|
|
230
|
-
- `
|
|
225
|
+
- `GET /api/v1/blockchain/braps/quote/` - Get swap/bridge quotes
|
|
231
226
|
|
|
232
227
|
## Error Handling
|
|
233
228
|
|
|
@@ -8,7 +8,10 @@ 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
|
|
11
|
+
from wayfinder_paths.core.clients.BRAPClient import (
|
|
12
|
+
BRAPClient,
|
|
13
|
+
BRAPQuoteResponse,
|
|
14
|
+
)
|
|
12
15
|
from wayfinder_paths.core.clients.LedgerClient import TransactionRecord
|
|
13
16
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
14
17
|
from wayfinder_paths.core.constants import DEFAULT_SLIPPAGE, ZERO_ADDRESS
|
|
@@ -61,7 +64,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
61
64
|
amount: str,
|
|
62
65
|
slippage: float | None = None,
|
|
63
66
|
wayfinder_fee: float | None = None,
|
|
64
|
-
) -> tuple[bool,
|
|
67
|
+
) -> tuple[bool, BRAPQuoteResponse | str]:
|
|
65
68
|
"""
|
|
66
69
|
Get a quote for a cross-chain swap operation.
|
|
67
70
|
|
|
@@ -77,19 +80,16 @@ class BRAPAdapter(BaseAdapter):
|
|
|
77
80
|
wayfinder_fee: Wayfinder fee (optional)
|
|
78
81
|
|
|
79
82
|
Returns:
|
|
80
|
-
Tuple of (success, data) where data is quote
|
|
83
|
+
Tuple of (success, data) where data is quote response or error message
|
|
81
84
|
"""
|
|
82
85
|
try:
|
|
83
86
|
data = await self.brap_client.get_quote(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
amount1=amount,
|
|
91
|
-
slippage=slippage,
|
|
92
|
-
wayfinder_fee=wayfinder_fee,
|
|
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
93
|
)
|
|
94
94
|
return (True, data)
|
|
95
95
|
except Exception as e:
|
|
@@ -130,22 +130,28 @@ class BRAPAdapter(BaseAdapter):
|
|
|
130
130
|
"""
|
|
131
131
|
try:
|
|
132
132
|
data = await self.brap_client.get_quote(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
amount1=amount,
|
|
140
|
-
slippage=slippage,
|
|
141
|
-
wayfinder_fee=wayfinder_fee,
|
|
133
|
+
from_token=from_token_address,
|
|
134
|
+
to_token=to_token_address,
|
|
135
|
+
from_chain=from_chain_id,
|
|
136
|
+
to_chain=to_chain_id,
|
|
137
|
+
from_wallet=from_address,
|
|
138
|
+
from_amount=amount,
|
|
142
139
|
)
|
|
143
140
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"
|
|
148
|
-
|
|
141
|
+
raw_quotes = data.get("quotes")
|
|
142
|
+
if isinstance(raw_quotes, list):
|
|
143
|
+
all_quotes = raw_quotes
|
|
144
|
+
best_quote = data.get("best_quote") or data.get("best_route")
|
|
145
|
+
else:
|
|
146
|
+
quotes_container = raw_quotes or {}
|
|
147
|
+
all_quotes = quotes_container.get(
|
|
148
|
+
"all_quotes", []
|
|
149
|
+
) or quotes_container.get("quotes", [])
|
|
150
|
+
best_quote = (
|
|
151
|
+
quotes_container.get("best_quote")
|
|
152
|
+
or data.get("best_quote")
|
|
153
|
+
or data.get("best_route")
|
|
154
|
+
)
|
|
149
155
|
|
|
150
156
|
# If preferred providers specified, select by provider preference
|
|
151
157
|
if preferred_providers and all_quotes:
|
|
@@ -156,13 +162,6 @@ class BRAPAdapter(BaseAdapter):
|
|
|
156
162
|
return (True, selected_quote)
|
|
157
163
|
# Fall through to best_quote if no preferred provider found
|
|
158
164
|
|
|
159
|
-
# Extract best quote from response - check nested quotes first, then top level
|
|
160
|
-
best_quote = (
|
|
161
|
-
quotes_container.get("best_quote")
|
|
162
|
-
or data.get("best_quote")
|
|
163
|
-
or data.get("best_route")
|
|
164
|
-
)
|
|
165
|
-
|
|
166
165
|
if not best_quote:
|
|
167
166
|
return (False, "No quotes available")
|
|
168
167
|
|
|
@@ -253,8 +252,8 @@ class BRAPAdapter(BaseAdapter):
|
|
|
253
252
|
to_token_address=to_token_address,
|
|
254
253
|
from_chain_id=from_chain_id,
|
|
255
254
|
to_chain_id=to_chain_id,
|
|
256
|
-
from_address="0x0000000000000000000000000000000000000000",
|
|
257
|
-
to_address="0x0000000000000000000000000000000000000000",
|
|
255
|
+
from_address="0x0000000000000000000000000000000000000000",
|
|
256
|
+
to_address="0x0000000000000000000000000000000000000000",
|
|
258
257
|
amount=amount,
|
|
259
258
|
slippage=slippage,
|
|
260
259
|
)
|
|
@@ -262,22 +261,22 @@ class BRAPAdapter(BaseAdapter):
|
|
|
262
261
|
if not success:
|
|
263
262
|
return (False, quote_data)
|
|
264
263
|
|
|
265
|
-
|
|
266
|
-
best_quote = quotes.get("best_quote")
|
|
264
|
+
best_quote = quote_data.get("best_quote")
|
|
267
265
|
|
|
268
266
|
if not best_quote:
|
|
269
267
|
return (False, "No quote available for fee calculation")
|
|
270
268
|
|
|
271
269
|
# Extract fee information
|
|
270
|
+
fee_estimate = best_quote.get("fee_estimate", {})
|
|
272
271
|
fees = {
|
|
273
272
|
"input_amount": best_quote.get("input_amount", 0),
|
|
274
273
|
"output_amount": best_quote.get("output_amount", 0),
|
|
275
|
-
"gas_fee": best_quote.get("
|
|
276
|
-
"bridge_fee":
|
|
277
|
-
"protocol_fee":
|
|
278
|
-
"total_fee":
|
|
279
|
-
"slippage":
|
|
280
|
-
"price_impact": best_quote.get("
|
|
274
|
+
"gas_fee": best_quote.get("gas_estimate") or 0,
|
|
275
|
+
"bridge_fee": 0,
|
|
276
|
+
"protocol_fee": fee_estimate.get("fee_total_usd", 0),
|
|
277
|
+
"total_fee": fee_estimate.get("fee_total_usd", 0),
|
|
278
|
+
"slippage": 0,
|
|
279
|
+
"price_impact": best_quote.get("quote", {}).get("priceImpact", 0),
|
|
281
280
|
}
|
|
282
281
|
|
|
283
282
|
return (True, fees)
|
|
@@ -310,19 +309,26 @@ class BRAPAdapter(BaseAdapter):
|
|
|
310
309
|
"""
|
|
311
310
|
try:
|
|
312
311
|
data = await self.brap_client.get_quote(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
amount1=amount,
|
|
320
|
-
slippage=slippage,
|
|
312
|
+
from_token=from_token_address,
|
|
313
|
+
to_token=to_token_address,
|
|
314
|
+
from_chain=from_chain_id,
|
|
315
|
+
to_chain=to_chain_id,
|
|
316
|
+
from_wallet="0x0000000000000000000000000000000000000000",
|
|
317
|
+
from_amount=amount,
|
|
321
318
|
)
|
|
322
319
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
320
|
+
raw_quotes = data.get("quotes")
|
|
321
|
+
if isinstance(raw_quotes, list):
|
|
322
|
+
all_quotes = raw_quotes
|
|
323
|
+
best_quote = data.get("best_quote") or data.get("best_route")
|
|
324
|
+
else:
|
|
325
|
+
quotes = raw_quotes or {}
|
|
326
|
+
all_quotes = quotes.get("all_quotes", []) or quotes.get("quotes", [])
|
|
327
|
+
best_quote = (
|
|
328
|
+
quotes.get("best_quote")
|
|
329
|
+
or data.get("best_quote")
|
|
330
|
+
or data.get("best_route")
|
|
331
|
+
)
|
|
326
332
|
|
|
327
333
|
if not all_quotes:
|
|
328
334
|
return (False, "No routes available")
|
|
@@ -339,7 +345,12 @@ class BRAPAdapter(BaseAdapter):
|
|
|
339
345
|
"route_analysis": {
|
|
340
346
|
"highest_output": sorted_quotes[0] if sorted_quotes else None,
|
|
341
347
|
"lowest_fees": (
|
|
342
|
-
min(
|
|
348
|
+
min(
|
|
349
|
+
all_quotes,
|
|
350
|
+
key=lambda x: float(
|
|
351
|
+
x.get("fee_estimate", {}).get("fee_total_usd", 0)
|
|
352
|
+
),
|
|
353
|
+
)
|
|
343
354
|
if all_quotes
|
|
344
355
|
else None
|
|
345
356
|
),
|
|
@@ -422,10 +433,13 @@ class BRAPAdapter(BaseAdapter):
|
|
|
422
433
|
chain = from_token.get("chain") or {}
|
|
423
434
|
chain_id = self._chain_id(chain)
|
|
424
435
|
|
|
425
|
-
|
|
426
|
-
|
|
436
|
+
calldata = quote.get("calldata") or {}
|
|
437
|
+
transaction = dict(calldata)
|
|
438
|
+
if not transaction or not transaction.get("data"):
|
|
427
439
|
return (False, "Quote missing calldata")
|
|
428
440
|
transaction["chainId"] = chain_id
|
|
441
|
+
# Always set the sender to the strategy wallet for broadcast.
|
|
442
|
+
# (Calldata may include either "from" or "from_address" depending on provider.)
|
|
429
443
|
transaction["from"] = to_checksum_address(from_address)
|
|
430
444
|
|
|
431
445
|
spender = transaction.get("to")
|
|
@@ -516,8 +530,8 @@ class BRAPAdapter(BaseAdapter):
|
|
|
516
530
|
to_token_address=to_token_address,
|
|
517
531
|
from_chain_id=from_chain_id,
|
|
518
532
|
to_chain_id=to_chain_id,
|
|
519
|
-
from_address="0x0000000000000000000000000000000000000000",
|
|
520
|
-
to_address="0x0000000000000000000000000000000000000000",
|
|
533
|
+
from_address="0x0000000000000000000000000000000000000000",
|
|
534
|
+
to_address="0x0000000000000000000000000000000000000000",
|
|
521
535
|
amount=amount,
|
|
522
536
|
slippage=slippage,
|
|
523
537
|
)
|
|
@@ -634,14 +648,15 @@ class BRAPAdapter(BaseAdapter):
|
|
|
634
648
|
validation_errors.append(f"Swap not possible: {quote_data}")
|
|
635
649
|
return (False, {"valid": False, "errors": validation_errors})
|
|
636
650
|
|
|
651
|
+
best_quote = (
|
|
652
|
+
quote_data.get("best_quote", {}) if isinstance(quote_data, dict) else {}
|
|
653
|
+
)
|
|
637
654
|
return (
|
|
638
655
|
True,
|
|
639
656
|
{
|
|
640
657
|
"valid": True,
|
|
641
658
|
"quote_available": True,
|
|
642
|
-
"estimated_output":
|
|
643
|
-
.get("best_quote", {})
|
|
644
|
-
.get("output_amount", "0"),
|
|
659
|
+
"estimated_output": str(best_quote.get("output_amount", 0)),
|
|
645
660
|
},
|
|
646
661
|
)
|
|
647
662
|
except Exception as e:
|
|
@@ -779,7 +794,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
779
794
|
|
|
780
795
|
def _chain_id(self, chain: Any) -> int:
|
|
781
796
|
if isinstance(chain, dict):
|
|
782
|
-
chain_id = chain.get("id")
|
|
797
|
+
chain_id = chain.get("id")
|
|
783
798
|
else:
|
|
784
799
|
chain_id = getattr(chain, "id", None)
|
|
785
800
|
if chain_id is None:
|