wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.25__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 +2 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/__init__.py +2 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +1 -1
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -2
- wayfinder_paths/core/clients/protocols.py +21 -22
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +12 -0
- wayfinder_paths/core/constants/__init__.py +15 -0
- wayfinder_paths/core/constants/base.py +6 -1
- wayfinder_paths/core/constants/contracts.py +39 -26
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -61
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/transaction.py +44 -1
- wayfinder_paths/core/utils/web3.py +3 -0
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.25.dist-info/METADATA +377 -0
- wayfinder_paths-0.1.25.dist-info/RECORD +185 -0
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
- wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hyperliquid operations for BorosHypeStrategy.
|
|
3
|
+
|
|
4
|
+
Kept as a mixin so the main strategy file stays readable without changing behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.adapter import (
|
|
14
|
+
HYPERLIQUID_BRIDGE_ADDRESS,
|
|
15
|
+
)
|
|
16
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.paired_filler import (
|
|
17
|
+
MIN_NOTIONAL_USD,
|
|
18
|
+
PairedFiller,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .constants import MAX_HL_LEVERAGE, USDC_ARB
|
|
22
|
+
from .types import Inventory
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BorosHypeHyperliquidOpsMixin:
|
|
26
|
+
async def _get_hype_asset_ids(self) -> tuple[int, int]:
|
|
27
|
+
if not self.hyperliquid_adapter:
|
|
28
|
+
raise RuntimeError("Hyperliquid adapter not configured")
|
|
29
|
+
|
|
30
|
+
perp_asset_id = self.hyperliquid_adapter.coin_to_asset.get("HYPE")
|
|
31
|
+
if perp_asset_id is None:
|
|
32
|
+
raise RuntimeError("HYPE perp asset ID not found")
|
|
33
|
+
|
|
34
|
+
spot_asset_id = await self.hyperliquid_adapter.get_spot_asset_id("HYPE", "USDC")
|
|
35
|
+
if spot_asset_id is None:
|
|
36
|
+
success, spot_assets = await self.hyperliquid_adapter.get_spot_assets()
|
|
37
|
+
if success and isinstance(spot_assets, dict):
|
|
38
|
+
spot_asset_id = spot_assets.get("HYPE/USDC")
|
|
39
|
+
|
|
40
|
+
if spot_asset_id is None:
|
|
41
|
+
raise RuntimeError("HYPE/USDC spot asset ID not found")
|
|
42
|
+
|
|
43
|
+
return int(spot_asset_id), int(perp_asset_id)
|
|
44
|
+
|
|
45
|
+
async def _ensure_hl_hype_leverage_set(self, address: str) -> tuple[bool, str]:
|
|
46
|
+
if self.simulation:
|
|
47
|
+
self._planner_runtime.leverage_set_for_hype = True
|
|
48
|
+
return True, "[SIMULATION] HYPE leverage set"
|
|
49
|
+
|
|
50
|
+
if not self.hyperliquid_adapter:
|
|
51
|
+
return False, "Hyperliquid adapter not configured"
|
|
52
|
+
if not self._sign_callback:
|
|
53
|
+
return False, "No strategy wallet signing callback configured"
|
|
54
|
+
|
|
55
|
+
if self._planner_runtime.leverage_set_for_hype:
|
|
56
|
+
return True, "HYPE leverage already set"
|
|
57
|
+
|
|
58
|
+
perp_asset_id = self.hyperliquid_adapter.coin_to_asset.get("HYPE")
|
|
59
|
+
if perp_asset_id is None:
|
|
60
|
+
return False, "HYPE perp asset ID not found"
|
|
61
|
+
|
|
62
|
+
ok_lev, lev_res = await self.hyperliquid_adapter.update_leverage(
|
|
63
|
+
asset_id=int(perp_asset_id),
|
|
64
|
+
leverage=int(MAX_HL_LEVERAGE),
|
|
65
|
+
is_cross=True,
|
|
66
|
+
address=address,
|
|
67
|
+
)
|
|
68
|
+
if not ok_lev:
|
|
69
|
+
return False, f"Failed to update Hyperliquid leverage: {lev_res}"
|
|
70
|
+
|
|
71
|
+
self._planner_runtime.leverage_set_for_hype = True
|
|
72
|
+
logger.info(f"Set Hyperliquid HYPE leverage to {int(MAX_HL_LEVERAGE)}x (cross)")
|
|
73
|
+
return True, f"Set Hyperliquid HYPE leverage to {int(MAX_HL_LEVERAGE)}x (cross)"
|
|
74
|
+
|
|
75
|
+
async def _cancel_lingering_orders(
|
|
76
|
+
self, pointers: list[dict[str, Any]], address: str
|
|
77
|
+
) -> None:
|
|
78
|
+
if not self.hyperliquid_adapter:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
for pointer in pointers:
|
|
82
|
+
metadata = pointer.get("metadata") if isinstance(pointer, dict) else None
|
|
83
|
+
if not isinstance(metadata, dict):
|
|
84
|
+
continue
|
|
85
|
+
asset_id = metadata.get("asset_id")
|
|
86
|
+
cloid = metadata.get("client_id")
|
|
87
|
+
if asset_id is None or not cloid:
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
await self.hyperliquid_adapter.cancel_order_by_cloid(
|
|
91
|
+
int(asset_id), str(cloid), address
|
|
92
|
+
)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
logger.debug(
|
|
95
|
+
f"Failed to cancel lingering order: asset_id={asset_id}, cloid={cloid}, err={exc}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def _cancel_hl_open_orders_for_hype(self, address: str) -> None:
|
|
99
|
+
if not self.hyperliquid_adapter:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
spot_asset_id, perp_asset_id = await self._get_hype_asset_ids()
|
|
104
|
+
except Exception as exc: # noqa: BLE001
|
|
105
|
+
logger.warning(f"Failed to resolve HYPE asset ids for cancel: {exc}")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
spot_coin = f"@{spot_asset_id - 10000}" if spot_asset_id >= 10000 else None
|
|
109
|
+
|
|
110
|
+
success, open_orders = await self.hyperliquid_adapter.get_frontend_open_orders(
|
|
111
|
+
address
|
|
112
|
+
)
|
|
113
|
+
if not success or not isinstance(open_orders, list):
|
|
114
|
+
logger.warning("Could not fetch Hyperliquid open orders to cancel")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
canceled = 0
|
|
118
|
+
for order in open_orders:
|
|
119
|
+
if not isinstance(order, dict):
|
|
120
|
+
continue
|
|
121
|
+
order_coin = order.get("coin") or order.get("asset") or ""
|
|
122
|
+
order_id = order.get("oid") or order.get("orderId") or order.get("id")
|
|
123
|
+
if not order_id:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
if str(order_coin) == "HYPE":
|
|
128
|
+
await self.hyperliquid_adapter.cancel_order(
|
|
129
|
+
asset_id=int(perp_asset_id),
|
|
130
|
+
order_id=order_id,
|
|
131
|
+
address=address,
|
|
132
|
+
)
|
|
133
|
+
canceled += 1
|
|
134
|
+
elif spot_coin and str(order_coin) == spot_coin:
|
|
135
|
+
await self.hyperliquid_adapter.cancel_order(
|
|
136
|
+
asset_id=int(spot_asset_id),
|
|
137
|
+
order_id=order_id,
|
|
138
|
+
address=address,
|
|
139
|
+
)
|
|
140
|
+
canceled += 1
|
|
141
|
+
except Exception as exc: # noqa: BLE001
|
|
142
|
+
logger.debug(
|
|
143
|
+
f"Failed cancel HL order: coin={order_coin} oid={order_id} err={exc}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if canceled:
|
|
147
|
+
logger.info(f"Canceled {canceled} Hyperliquid HYPE order(s)")
|
|
148
|
+
|
|
149
|
+
async def _deploy_excess_hl_margin(
|
|
150
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
151
|
+
) -> tuple[bool, str]:
|
|
152
|
+
# Flow: Transfer USDC perp→spot, buy HYPE on spot, bridge to HyperEVM
|
|
153
|
+
excess_margin = params.get("excess_margin_usd", 0)
|
|
154
|
+
|
|
155
|
+
if excess_margin < 5:
|
|
156
|
+
return True, "Skipping small excess margin deployment"
|
|
157
|
+
|
|
158
|
+
if not self.hyperliquid_adapter:
|
|
159
|
+
return False, "Hyperliquid adapter not configured"
|
|
160
|
+
|
|
161
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
162
|
+
address = strategy_wallet.get("address")
|
|
163
|
+
if not address:
|
|
164
|
+
return False, "No strategy wallet address configured"
|
|
165
|
+
|
|
166
|
+
if self.simulation:
|
|
167
|
+
return (
|
|
168
|
+
True,
|
|
169
|
+
f"[SIMULATION] Deployed ${excess_margin:.2f} excess HL margin to spot",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
hype_price = float(inventory.hype_price_usd or 0.0)
|
|
173
|
+
if hype_price <= 0:
|
|
174
|
+
hype_price = 25.0
|
|
175
|
+
|
|
176
|
+
success, result = await self.hyperliquid_adapter.transfer_perp_to_spot(
|
|
177
|
+
amount=excess_margin,
|
|
178
|
+
address=address,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if not success:
|
|
182
|
+
error_msg = result if isinstance(result, str) else str(result)
|
|
183
|
+
return False, f"Perp to spot transfer failed: {error_msg}"
|
|
184
|
+
|
|
185
|
+
logger.info(f"Transferred ${excess_margin:.2f} from perp margin to spot")
|
|
186
|
+
|
|
187
|
+
if excess_margin < MIN_NOTIONAL_USD:
|
|
188
|
+
return True, f"Excess margin ${excess_margin:.2f} too small for paired fill"
|
|
189
|
+
|
|
190
|
+
hype_to_buy = (excess_margin * 0.98) / hype_price # 2% buffer
|
|
191
|
+
|
|
192
|
+
spot_asset_id, perp_asset_id = await self._get_hype_asset_ids()
|
|
193
|
+
paired_filler = PairedFiller(adapter=self.hyperliquid_adapter, address=address)
|
|
194
|
+
|
|
195
|
+
ok_lev, lev_msg = await self._ensure_hl_hype_leverage_set(address)
|
|
196
|
+
if not ok_lev:
|
|
197
|
+
return False, lev_msg
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
(
|
|
201
|
+
filled_spot,
|
|
202
|
+
filled_perp,
|
|
203
|
+
spot_notional,
|
|
204
|
+
perp_notional,
|
|
205
|
+
spot_pointers,
|
|
206
|
+
perp_pointers,
|
|
207
|
+
) = await paired_filler.fill_pair_units(
|
|
208
|
+
coin="HYPE",
|
|
209
|
+
spot_asset_id=spot_asset_id,
|
|
210
|
+
perp_asset_id=perp_asset_id,
|
|
211
|
+
total_units=hype_to_buy,
|
|
212
|
+
direction="long_spot_short_perp",
|
|
213
|
+
builder_fee=self.builder_fee,
|
|
214
|
+
)
|
|
215
|
+
logger.info(
|
|
216
|
+
f"Paired fill from excess margin complete: "
|
|
217
|
+
f"spot={filled_spot:.4f} (${spot_notional:.2f}), "
|
|
218
|
+
f"perp={filled_perp:.4f} (${perp_notional:.2f})"
|
|
219
|
+
)
|
|
220
|
+
await self._cancel_lingering_orders(spot_pointers + perp_pointers, address)
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
logger.error(f"Paired fill from excess margin failed: {exc}")
|
|
223
|
+
return False, f"Paired fill failed: {exc}"
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
227
|
+
address
|
|
228
|
+
)
|
|
229
|
+
if not success or not isinstance(spot_state, dict):
|
|
230
|
+
return (
|
|
231
|
+
True,
|
|
232
|
+
f"Deployed ${excess_margin:.2f} (bridge pending spot state)",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
balances = spot_state.get("balances", [])
|
|
236
|
+
actual_hype = 0.0
|
|
237
|
+
for bal in balances:
|
|
238
|
+
coin = bal.get("coin") or bal.get("token")
|
|
239
|
+
if coin != "HYPE":
|
|
240
|
+
continue
|
|
241
|
+
hold = float(bal.get("hold", 0))
|
|
242
|
+
total = float(bal.get("total", 0))
|
|
243
|
+
actual_hype = max(0.0, total - hold)
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
amount_to_bridge = actual_hype - 0.001
|
|
247
|
+
if amount_to_bridge < 0.1:
|
|
248
|
+
return True, f"Deployed ${excess_margin:.2f} (no HYPE to bridge)"
|
|
249
|
+
|
|
250
|
+
ok, res = await self.hyperliquid_adapter.hypercore_to_hyperevm(
|
|
251
|
+
amount=amount_to_bridge,
|
|
252
|
+
address=address,
|
|
253
|
+
)
|
|
254
|
+
if ok:
|
|
255
|
+
return (
|
|
256
|
+
True,
|
|
257
|
+
f"Deployed ${excess_margin:.2f} excess margin → {amount_to_bridge:.4f} HYPE to HyperEVM",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
err = res if isinstance(res, str) else str(res)
|
|
261
|
+
return False, f"Bridge failed: {err}"
|
|
262
|
+
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
return False, f"Bridge failed: {exc}"
|
|
265
|
+
|
|
266
|
+
async def _transfer_hl_spot_to_hyperevm(
|
|
267
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
268
|
+
) -> tuple[bool, str]:
|
|
269
|
+
# HL spot HYPE withdrawal goes directly to HyperEVM (native chain) via L1 withdrawal
|
|
270
|
+
hype_amount = params.get("hype_amount", 0)
|
|
271
|
+
|
|
272
|
+
if hype_amount < 0.1:
|
|
273
|
+
return True, "Skipping small HYPE transfer"
|
|
274
|
+
|
|
275
|
+
if not self.hyperliquid_adapter:
|
|
276
|
+
return False, "Hyperliquid adapter not configured"
|
|
277
|
+
|
|
278
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
279
|
+
address = strategy_wallet.get("address")
|
|
280
|
+
if not address:
|
|
281
|
+
return False, "No strategy wallet address configured"
|
|
282
|
+
|
|
283
|
+
if self.simulation:
|
|
284
|
+
return (
|
|
285
|
+
True,
|
|
286
|
+
f"[SIMULATION] Transferred {hype_amount:.4f} HYPE from HL spot to HyperEVM",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
291
|
+
address
|
|
292
|
+
)
|
|
293
|
+
if not success or not isinstance(spot_state, dict):
|
|
294
|
+
return False, "Failed to read HL spot balances"
|
|
295
|
+
|
|
296
|
+
balances = spot_state.get("balances", [])
|
|
297
|
+
actual_hype = 0.0
|
|
298
|
+
for bal in balances:
|
|
299
|
+
coin = bal.get("coin") or bal.get("token")
|
|
300
|
+
if coin != "HYPE":
|
|
301
|
+
continue
|
|
302
|
+
hold = float(bal.get("hold", 0))
|
|
303
|
+
total = float(bal.get("total", 0))
|
|
304
|
+
actual_hype = max(0.0, total - hold)
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
transfer_amount = actual_hype - 0.001
|
|
308
|
+
if transfer_amount < 0.1:
|
|
309
|
+
return True, "Insufficient HYPE balance to transfer"
|
|
310
|
+
|
|
311
|
+
ok, res = await self.hyperliquid_adapter.hypercore_to_hyperevm(
|
|
312
|
+
amount=transfer_amount,
|
|
313
|
+
address=address,
|
|
314
|
+
)
|
|
315
|
+
if ok:
|
|
316
|
+
return True, f"Transferred {transfer_amount:.4f} HYPE to HyperEVM"
|
|
317
|
+
|
|
318
|
+
err = res if isinstance(res, str) else str(res)
|
|
319
|
+
return False, f"Transfer failed: {err}"
|
|
320
|
+
|
|
321
|
+
except Exception as exc:
|
|
322
|
+
logger.error(f"HYPE spot transfer failed: {exc}")
|
|
323
|
+
return False, f"HL spot transfer failed: {exc}"
|
|
324
|
+
|
|
325
|
+
async def _ensure_hl_short(
|
|
326
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
327
|
+
) -> tuple[bool, str]:
|
|
328
|
+
# Safety: 2x leverage, venue-valid rounding, check free margin before increasing
|
|
329
|
+
target_size = float(params.get("target_size") or 0.0)
|
|
330
|
+
current_size = float(params.get("current_size") or 0.0)
|
|
331
|
+
|
|
332
|
+
# If both target and current are negligible, do nothing.
|
|
333
|
+
# If target is ~0 but we still have a short, fall through so we can close it.
|
|
334
|
+
if target_size < 0.01 and abs(current_size) < 0.01:
|
|
335
|
+
return True, "No HYPE exposure to hedge"
|
|
336
|
+
|
|
337
|
+
tol = max(
|
|
338
|
+
self._planner_config.delta_neutral_abs_tol_hype,
|
|
339
|
+
target_size * self._planner_config.delta_neutral_rel_tol,
|
|
340
|
+
)
|
|
341
|
+
delta = target_size - current_size
|
|
342
|
+
diff = abs(delta)
|
|
343
|
+
if diff <= tol:
|
|
344
|
+
return (
|
|
345
|
+
True,
|
|
346
|
+
f"HYPE hedge within tolerance: Δ={diff:.4f} tol={tol:.4f} "
|
|
347
|
+
f"(spot={target_size:.4f}, short={current_size:.4f})",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if not self.hyperliquid_adapter:
|
|
351
|
+
return False, "Hyperliquid adapter not configured"
|
|
352
|
+
|
|
353
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
354
|
+
address = strategy_wallet.get("address")
|
|
355
|
+
if not address:
|
|
356
|
+
return False, "No strategy wallet address configured"
|
|
357
|
+
|
|
358
|
+
perp_asset_id = self.hyperliquid_adapter.coin_to_asset.get("HYPE")
|
|
359
|
+
if perp_asset_id is None:
|
|
360
|
+
return False, "HYPE perp asset ID not found"
|
|
361
|
+
|
|
362
|
+
ok_lev, lev_msg = await self._ensure_hl_hype_leverage_set(address)
|
|
363
|
+
if not ok_lev:
|
|
364
|
+
return False, lev_msg
|
|
365
|
+
|
|
366
|
+
hype_price = float(inventory.hype_price_usd or 0.0)
|
|
367
|
+
if hype_price <= 0:
|
|
368
|
+
return False, "Cannot hedge: HYPE price unavailable"
|
|
369
|
+
|
|
370
|
+
# delta > 0 => need to INCREASE short (sell more)
|
|
371
|
+
if delta > 0:
|
|
372
|
+
min_increase_needed = max(0.0, diff - tol)
|
|
373
|
+
free_margin = float(inventory.hl_withdrawable_usd or 0.0)
|
|
374
|
+
max_increase_by_margin = (
|
|
375
|
+
(free_margin * MAX_HL_LEVERAGE) / hype_price if hype_price > 0 else 0.0
|
|
376
|
+
)
|
|
377
|
+
desired_increase = min(diff, max_increase_by_margin)
|
|
378
|
+
required_margin = (min_increase_needed * hype_price) / MAX_HL_LEVERAGE
|
|
379
|
+
|
|
380
|
+
if free_margin < required_margin * 0.9:
|
|
381
|
+
return (
|
|
382
|
+
False,
|
|
383
|
+
f"Insufficient free margin (${free_margin:.2f}) to increase short by {diff:.4f} HYPE "
|
|
384
|
+
f"(need ${required_margin:.2f}, tol={tol:.4f}). Consider trimming spot.",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
rounded_size = self.hyperliquid_adapter.get_valid_order_size(
|
|
388
|
+
int(perp_asset_id), desired_increase
|
|
389
|
+
)
|
|
390
|
+
if rounded_size <= 0:
|
|
391
|
+
return (
|
|
392
|
+
False,
|
|
393
|
+
f"Hedge mismatch Δ={diff:.4f} tol={tol:.4f} but order size rounds to 0.",
|
|
394
|
+
)
|
|
395
|
+
if rounded_size + 1e-9 < min_increase_needed:
|
|
396
|
+
return (
|
|
397
|
+
False,
|
|
398
|
+
"Insufficient free margin to hedge within tolerance after rounding "
|
|
399
|
+
f"(need ≥{min_increase_needed:.4f} HYPE, got {rounded_size:.4f}).",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
order_value_usd = rounded_size * hype_price
|
|
403
|
+
if order_value_usd < float(MIN_NOTIONAL_USD):
|
|
404
|
+
return True, (
|
|
405
|
+
f"Delta {diff:.4f} HYPE below HL ${MIN_NOTIONAL_USD:.0f} minimum, acceptable"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
if self.simulation:
|
|
409
|
+
return True, (
|
|
410
|
+
f"[SIMULATION] Increased HYPE short by {rounded_size:.4f} "
|
|
411
|
+
f"(target={target_size:.4f}, current={current_size:.4f})"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
ok, res = await self.hyperliquid_adapter.place_market_order(
|
|
415
|
+
asset_id=int(perp_asset_id),
|
|
416
|
+
is_buy=False, # sell to increase short
|
|
417
|
+
slippage=0.05,
|
|
418
|
+
size=float(rounded_size),
|
|
419
|
+
address=address,
|
|
420
|
+
builder=self.builder_fee,
|
|
421
|
+
)
|
|
422
|
+
if not ok:
|
|
423
|
+
return False, f"Failed to increase HYPE short: {res}"
|
|
424
|
+
|
|
425
|
+
return True, f"Increased HYPE short by {rounded_size:.4f}"
|
|
426
|
+
|
|
427
|
+
# delta < 0 => need to REDUCE short (buy back)
|
|
428
|
+
reduce_units = min(diff, current_size)
|
|
429
|
+
rounded_size = self.hyperliquid_adapter.get_valid_order_size(
|
|
430
|
+
int(perp_asset_id), reduce_units
|
|
431
|
+
)
|
|
432
|
+
if rounded_size <= 0:
|
|
433
|
+
return True, "No short position to reduce"
|
|
434
|
+
|
|
435
|
+
order_value_usd = rounded_size * hype_price
|
|
436
|
+
if order_value_usd < float(MIN_NOTIONAL_USD):
|
|
437
|
+
return True, (
|
|
438
|
+
f"Delta {diff:.4f} HYPE below HL ${MIN_NOTIONAL_USD:.0f} minimum, acceptable"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if self.simulation:
|
|
442
|
+
return True, (
|
|
443
|
+
f"[SIMULATION] Reduced HYPE short by {rounded_size:.4f} "
|
|
444
|
+
f"(target={target_size:.4f}, current={current_size:.4f})"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
ok, res = await self.hyperliquid_adapter.place_market_order(
|
|
448
|
+
asset_id=int(perp_asset_id),
|
|
449
|
+
is_buy=True, # buy to reduce short
|
|
450
|
+
slippage=0.05,
|
|
451
|
+
size=float(rounded_size),
|
|
452
|
+
address=address,
|
|
453
|
+
reduce_only=True,
|
|
454
|
+
builder=self.builder_fee,
|
|
455
|
+
)
|
|
456
|
+
if not ok:
|
|
457
|
+
return False, f"Failed to reduce HYPE short: {res}"
|
|
458
|
+
|
|
459
|
+
return True, f"Reduced HYPE short by {rounded_size:.4f}"
|
|
460
|
+
|
|
461
|
+
async def _send_usdc_to_hl(
|
|
462
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
463
|
+
) -> tuple[bool, str]:
|
|
464
|
+
amount_usd = params.get("amount_usd", 0.0)
|
|
465
|
+
|
|
466
|
+
if amount_usd < self._planner_config.min_usdc_action:
|
|
467
|
+
return True, f"Skipping small USDC send (${amount_usd:.2f})"
|
|
468
|
+
|
|
469
|
+
if not self.balance_adapter:
|
|
470
|
+
return False, "Balance adapter not configured"
|
|
471
|
+
|
|
472
|
+
if not self.hyperliquid_adapter:
|
|
473
|
+
return False, "Hyperliquid adapter not configured"
|
|
474
|
+
|
|
475
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
476
|
+
address = strategy_wallet.get("address")
|
|
477
|
+
if not address:
|
|
478
|
+
return False, "No strategy wallet address configured"
|
|
479
|
+
|
|
480
|
+
if self.simulation:
|
|
481
|
+
return True, f"[SIMULATION] Sent ${amount_usd:.2f} USDC to Hyperliquid"
|
|
482
|
+
|
|
483
|
+
usdc_raw = int(float(amount_usd) * 1e6)
|
|
484
|
+
success, result = await self.balance_adapter.send_to_address(
|
|
485
|
+
token_id=USDC_ARB,
|
|
486
|
+
amount=usdc_raw,
|
|
487
|
+
from_wallet=strategy_wallet,
|
|
488
|
+
to_address=HYPERLIQUID_BRIDGE_ADDRESS,
|
|
489
|
+
signing_callback=self._sign_callback,
|
|
490
|
+
)
|
|
491
|
+
if not success:
|
|
492
|
+
return False, f"Failed to send USDC to HL bridge: {result}"
|
|
493
|
+
|
|
494
|
+
confirmed, final_balance = await self.hyperliquid_adapter.wait_for_deposit(
|
|
495
|
+
address=address,
|
|
496
|
+
expected_increase=float(amount_usd),
|
|
497
|
+
timeout_s=240,
|
|
498
|
+
poll_interval_s=10,
|
|
499
|
+
)
|
|
500
|
+
if not confirmed:
|
|
501
|
+
return False, (
|
|
502
|
+
f"USDC sent to bridge but not confirmed on Hyperliquid within timeout. "
|
|
503
|
+
f"Current HL balance: ${final_balance:.2f}"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
True,
|
|
508
|
+
f"Sent ${amount_usd:.2f} USDC to Hyperliquid (balance=${final_balance:.2f})",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
async def _bridge_to_hyperevm(
|
|
512
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
513
|
+
) -> tuple[bool, str]:
|
|
514
|
+
# Assumes Arb→HL deposit handled by SEND_USDC_TO_HL. We: 1) xfer perp→spot,
|
|
515
|
+
# 2) paired fill (long spot / short perp), 3) bridge spot HYPE to HyperEVM.
|
|
516
|
+
desired_usd = float(params.get("amount_usd") or 0.0)
|
|
517
|
+
reserve_hl_margin_usd = float(params.get("reserve_hl_margin_usd") or 0.0)
|
|
518
|
+
|
|
519
|
+
if desired_usd < max(self._planner_config.min_usdc_action, MIN_NOTIONAL_USD):
|
|
520
|
+
# Return False to not trigger re-observation when nothing changes
|
|
521
|
+
return False, f"Skipping small bridge (${desired_usd:.2f})"
|
|
522
|
+
|
|
523
|
+
if not self.hyperliquid_adapter:
|
|
524
|
+
return False, "Hyperliquid adapter not configured"
|
|
525
|
+
|
|
526
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
527
|
+
address = strategy_wallet.get("address")
|
|
528
|
+
if not address:
|
|
529
|
+
return False, "No strategy wallet address configured"
|
|
530
|
+
|
|
531
|
+
if self.simulation:
|
|
532
|
+
return True, f"[SIMULATION] Bridged ${desired_usd:.2f} to HyperEVM"
|
|
533
|
+
|
|
534
|
+
hype_price = float(inventory.hype_price_usd or 0.0)
|
|
535
|
+
if hype_price <= 0:
|
|
536
|
+
success, prices = await self.hyperliquid_adapter.get_all_mid_prices()
|
|
537
|
+
if success and isinstance(prices, dict):
|
|
538
|
+
hype_price = float(prices.get("HYPE", 0.0))
|
|
539
|
+
if hype_price <= 0:
|
|
540
|
+
return False, "Could not determine HYPE price"
|
|
541
|
+
|
|
542
|
+
# Transfer from perp→spot, but never below the reserved perp margin.
|
|
543
|
+
spot_usdc_before = float(inventory.hl_spot_usdc or 0.0)
|
|
544
|
+
perp_margin = float(inventory.hl_perp_margin or 0.0)
|
|
545
|
+
withdrawable = float(inventory.hl_withdrawable_usd or 0.0)
|
|
546
|
+
|
|
547
|
+
transferable_from_perp = max(
|
|
548
|
+
0.0,
|
|
549
|
+
min(
|
|
550
|
+
withdrawable,
|
|
551
|
+
perp_margin - reserve_hl_margin_usd,
|
|
552
|
+
),
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
need_from_perp = max(0.0, desired_usd - spot_usdc_before)
|
|
556
|
+
xfer_usd = min(need_from_perp, transferable_from_perp)
|
|
557
|
+
|
|
558
|
+
if xfer_usd >= self._planner_config.min_usdc_action:
|
|
559
|
+
xfer_ok, xfer_res = await self.hyperliquid_adapter.transfer_perp_to_spot(
|
|
560
|
+
amount=float(xfer_usd),
|
|
561
|
+
address=address,
|
|
562
|
+
)
|
|
563
|
+
if not xfer_ok:
|
|
564
|
+
return False, f"Perp→spot transfer failed: {xfer_res}"
|
|
565
|
+
logger.info(
|
|
566
|
+
f"Transferred ${xfer_usd:.2f} from HL perp→spot "
|
|
567
|
+
f"(reserve_perp=${reserve_hl_margin_usd:.2f})"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
deployable_usd = min(desired_usd, spot_usdc_before + xfer_usd)
|
|
571
|
+
if deployable_usd < MIN_NOTIONAL_USD:
|
|
572
|
+
return False, (
|
|
573
|
+
"Insufficient deployable HL spot USDC after reserving perp margin "
|
|
574
|
+
f"(${deployable_usd:.2f} < ${MIN_NOTIONAL_USD:.2f}; "
|
|
575
|
+
f"reserve=${reserve_hl_margin_usd:.2f}, perp=${perp_margin:.2f}, "
|
|
576
|
+
f"withdrawable=${withdrawable:.2f}, spot=${spot_usdc_before:.2f})"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Atomic Paired Fill: buy spot HYPE, short perp HYPE
|
|
580
|
+
hype_units = (deployable_usd * 0.98) / hype_price # 2% buffer
|
|
581
|
+
spot_asset_id, perp_asset_id = await self._get_hype_asset_ids()
|
|
582
|
+
paired_filler = PairedFiller(adapter=self.hyperliquid_adapter, address=address)
|
|
583
|
+
|
|
584
|
+
ok_lev, lev_msg = await self._ensure_hl_hype_leverage_set(address)
|
|
585
|
+
if not ok_lev:
|
|
586
|
+
return False, lev_msg
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
(
|
|
590
|
+
filled_spot,
|
|
591
|
+
filled_perp,
|
|
592
|
+
spot_notional,
|
|
593
|
+
perp_notional,
|
|
594
|
+
spot_pointers,
|
|
595
|
+
perp_pointers,
|
|
596
|
+
) = await paired_filler.fill_pair_units(
|
|
597
|
+
coin="HYPE",
|
|
598
|
+
spot_asset_id=spot_asset_id,
|
|
599
|
+
perp_asset_id=perp_asset_id,
|
|
600
|
+
total_units=hype_units,
|
|
601
|
+
direction="long_spot_short_perp",
|
|
602
|
+
builder_fee=self.builder_fee,
|
|
603
|
+
)
|
|
604
|
+
logger.info(
|
|
605
|
+
f"Paired fill complete: spot={filled_spot:.4f} (${spot_notional:.2f}), "
|
|
606
|
+
f"perp={filled_perp:.4f} (${perp_notional:.2f})"
|
|
607
|
+
)
|
|
608
|
+
await self._cancel_lingering_orders(spot_pointers + perp_pointers, address)
|
|
609
|
+
except Exception as exc:
|
|
610
|
+
logger.error(f"Paired fill failed: {exc}")
|
|
611
|
+
return False, f"Paired fill failed: {exc}"
|
|
612
|
+
|
|
613
|
+
success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
614
|
+
address
|
|
615
|
+
)
|
|
616
|
+
if not success or not isinstance(spot_state, dict):
|
|
617
|
+
return False, "Failed to read HL spot balances after paired fill"
|
|
618
|
+
|
|
619
|
+
balances = spot_state.get("balances", [])
|
|
620
|
+
actual_hype = 0.0
|
|
621
|
+
for bal in balances:
|
|
622
|
+
coin = bal.get("coin") or bal.get("token")
|
|
623
|
+
if coin != "HYPE":
|
|
624
|
+
continue
|
|
625
|
+
hold = float(bal.get("hold", 0))
|
|
626
|
+
total = float(bal.get("total", 0))
|
|
627
|
+
actual_hype = max(0.0, total - hold)
|
|
628
|
+
break
|
|
629
|
+
|
|
630
|
+
amount_to_bridge = actual_hype - 0.001
|
|
631
|
+
if amount_to_bridge < 0.1:
|
|
632
|
+
return False, "No HYPE available to bridge to HyperEVM"
|
|
633
|
+
|
|
634
|
+
ok, res = await self.hyperliquid_adapter.hypercore_to_hyperevm(
|
|
635
|
+
amount=amount_to_bridge,
|
|
636
|
+
address=address,
|
|
637
|
+
)
|
|
638
|
+
if ok:
|
|
639
|
+
return True, f"Bridged {amount_to_bridge:.4f} HYPE to HyperEVM"
|
|
640
|
+
|
|
641
|
+
err = res if isinstance(res, str) else str(res)
|
|
642
|
+
return False, f"Bridge failed: {err}"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
schema_version: "0.1"
|
|
2
|
+
entrypoint: "wayfinder_paths.strategies.boros_hype_strategy.strategy.BorosHypeStrategy"
|
|
3
|
+
permissions:
|
|
4
|
+
policy: |
|
|
5
|
+
(wallet.id == 'FORMAT_WALLET_ID') AND (
|
|
6
|
+
# Allow Hyperliquid EIP-712 order/transfer actions
|
|
7
|
+
(action.type == 'hyperliquid_order') OR
|
|
8
|
+
(action.type == 'hyperliquid_cancel') OR
|
|
9
|
+
(action.type == 'hyperliquid_transfer') OR
|
|
10
|
+
(action.type == 'hyperliquid_withdraw') OR
|
|
11
|
+
# Allow USDC transfers to Hyperliquid bridge
|
|
12
|
+
(action.type == 'erc20_transfer' AND action.to == '0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7') OR
|
|
13
|
+
# Allow USDT transfers to Boros
|
|
14
|
+
(action.type == 'erc20_transfer' AND action.to IN boros_contracts) OR
|
|
15
|
+
# Allow Boros EIP-712 order actions
|
|
16
|
+
(action.type == 'boros_order') OR
|
|
17
|
+
(action.type == 'boros_deposit') OR
|
|
18
|
+
(action.type == 'boros_withdraw') OR
|
|
19
|
+
# Allow HyperEVM token swaps (kHYPE, lHYPE)
|
|
20
|
+
(action.type == 'swap' AND action.chain_id == 999) OR
|
|
21
|
+
# Allow withdrawals to main wallet
|
|
22
|
+
(action.type == 'erc20_transfer' AND action.to == main_wallet.address)
|
|
23
|
+
)
|
|
24
|
+
adapters:
|
|
25
|
+
- name: "BALANCE"
|
|
26
|
+
capabilities: ["wallet_read", "wallet_transfer"]
|
|
27
|
+
- name: "LEDGER"
|
|
28
|
+
capabilities: ["ledger.read", "ledger.write", "strategy.transactions"]
|
|
29
|
+
- name: "TOKEN"
|
|
30
|
+
capabilities: ["token.read"]
|
|
31
|
+
- name: "HYPERLIQUID"
|
|
32
|
+
capabilities: ["market.read", "market.meta", "market.funding", "market.candles", "market.orderbook", "order.execute", "order.cancel", "position.manage", "transfer", "withdraw"]
|
|
33
|
+
- name: "BOROS"
|
|
34
|
+
capabilities: ["market.read", "market.quote", "position.open", "position.close", "collateral.deposit", "collateral.withdraw"]
|
|
35
|
+
- name: "BRAP"
|
|
36
|
+
capabilities: ["swap.quote", "swap.execute", "bridge.execute"]
|