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,931 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.adapter import HyperliquidAdapter
|
|
8
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.executor import (
|
|
9
|
+
LocalHyperliquidExecutor,
|
|
10
|
+
)
|
|
11
|
+
from wayfinder_paths.core.constants.hyperliquid import (
|
|
12
|
+
DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP,
|
|
13
|
+
HYPE_FEE_WALLET,
|
|
14
|
+
)
|
|
15
|
+
from wayfinder_paths.mcp.preview import build_hyperliquid_execute_preview
|
|
16
|
+
from wayfinder_paths.mcp.state.profile_store import WalletProfileStore
|
|
17
|
+
from wayfinder_paths.mcp.state.store import IdempotencyStore
|
|
18
|
+
from wayfinder_paths.mcp.utils import (
|
|
19
|
+
err,
|
|
20
|
+
find_wallet_by_label,
|
|
21
|
+
load_config_json,
|
|
22
|
+
normalize_address,
|
|
23
|
+
ok,
|
|
24
|
+
sha256_json,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_PERP_SUFFIX_RE = re.compile(r"[-_ ]?perp$", re.IGNORECASE)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_wallet_address(
|
|
31
|
+
*, wallet_label: str | None, wallet_address: str | None
|
|
32
|
+
) -> str | None:
|
|
33
|
+
waddr = normalize_address(wallet_address)
|
|
34
|
+
if waddr:
|
|
35
|
+
return waddr
|
|
36
|
+
want = (wallet_label or "").strip()
|
|
37
|
+
if not want:
|
|
38
|
+
return None
|
|
39
|
+
w = find_wallet_by_label(want)
|
|
40
|
+
if not w:
|
|
41
|
+
return None
|
|
42
|
+
return normalize_address(w.get("address"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_builder_fee(
|
|
46
|
+
*,
|
|
47
|
+
config: dict[str, Any],
|
|
48
|
+
builder_fee_tenths_bp: int | None,
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Resolve builder fee config for Hyperliquid orders.
|
|
52
|
+
|
|
53
|
+
Builder attribution is **mandatory** and always uses the Wayfinder builder wallet.
|
|
54
|
+
Fee priority:
|
|
55
|
+
1) explicit builder_fee_tenths_bp
|
|
56
|
+
2) config["builder_fee"]["f"] (typically config.json["strategy"]["builder_fee"])
|
|
57
|
+
3) DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP
|
|
58
|
+
"""
|
|
59
|
+
expected_builder = HYPE_FEE_WALLET.lower()
|
|
60
|
+
fee = builder_fee_tenths_bp
|
|
61
|
+
if fee is None:
|
|
62
|
+
cfg = config.get("builder_fee") if isinstance(config, dict) else None
|
|
63
|
+
if isinstance(cfg, dict):
|
|
64
|
+
cfg_builder = str(cfg.get("b") or "").strip()
|
|
65
|
+
if cfg_builder and cfg_builder.lower() != expected_builder:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"config builder_fee.b must be {expected_builder} (got {cfg_builder})"
|
|
68
|
+
)
|
|
69
|
+
if cfg.get("f") is not None:
|
|
70
|
+
fee = cfg.get("f")
|
|
71
|
+
if fee is None:
|
|
72
|
+
fee = DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
fee_i = int(fee)
|
|
76
|
+
except (TypeError, ValueError) as exc:
|
|
77
|
+
raise ValueError("builder_fee_tenths_bp must be an int") from exc
|
|
78
|
+
if fee_i <= 0:
|
|
79
|
+
raise ValueError("builder_fee_tenths_bp must be > 0")
|
|
80
|
+
|
|
81
|
+
return {"b": expected_builder, "f": fee_i}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _resolve_perp_asset_id(
|
|
85
|
+
adapter: HyperliquidAdapter, *, coin: str | None, asset_id: int | None
|
|
86
|
+
) -> tuple[bool, int | dict[str, Any]]:
|
|
87
|
+
if asset_id is not None:
|
|
88
|
+
try:
|
|
89
|
+
return True, int(asset_id)
|
|
90
|
+
except (TypeError, ValueError):
|
|
91
|
+
return False, {"code": "invalid_request", "message": "asset_id must be int"}
|
|
92
|
+
|
|
93
|
+
c = (coin or "").strip()
|
|
94
|
+
if not c:
|
|
95
|
+
return False, {
|
|
96
|
+
"code": "invalid_request",
|
|
97
|
+
"message": "coin or asset_id is required",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
c = _PERP_SUFFIX_RE.sub("", c).strip()
|
|
101
|
+
if not c:
|
|
102
|
+
return False, {"code": "invalid_request", "message": "coin is required"}
|
|
103
|
+
|
|
104
|
+
mapping = adapter.coin_to_asset or {}
|
|
105
|
+
lower = {str(k).lower(): int(v) for k, v in mapping.items()}
|
|
106
|
+
aid = lower.get(c.lower())
|
|
107
|
+
if aid is None:
|
|
108
|
+
return (
|
|
109
|
+
False,
|
|
110
|
+
{
|
|
111
|
+
"code": "not_found",
|
|
112
|
+
"message": f"Unknown perp coin: {c}",
|
|
113
|
+
"details": {"coin": c},
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
return True, aid
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def hyperliquid(
|
|
120
|
+
action: Literal[
|
|
121
|
+
"user_state",
|
|
122
|
+
"spot_user_state",
|
|
123
|
+
"mid_prices",
|
|
124
|
+
"meta_and_asset_ctxs",
|
|
125
|
+
"spot_assets",
|
|
126
|
+
"l2_book",
|
|
127
|
+
"wait_for_deposit",
|
|
128
|
+
"wait_for_withdrawal",
|
|
129
|
+
],
|
|
130
|
+
*,
|
|
131
|
+
wallet_label: str | None = None,
|
|
132
|
+
wallet_address: str | None = None,
|
|
133
|
+
coin: str | None = None,
|
|
134
|
+
n_levels: int = 20,
|
|
135
|
+
expected_increase: float | None = None,
|
|
136
|
+
timeout_s: int = 120,
|
|
137
|
+
poll_interval_s: int = 5,
|
|
138
|
+
lookback_s: int = 5,
|
|
139
|
+
max_poll_time_s: int = 15 * 60,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
adapter = HyperliquidAdapter(simulation=True)
|
|
142
|
+
|
|
143
|
+
if action in {
|
|
144
|
+
"user_state",
|
|
145
|
+
"spot_user_state",
|
|
146
|
+
"wait_for_deposit",
|
|
147
|
+
"wait_for_withdrawal",
|
|
148
|
+
}:
|
|
149
|
+
addr = _resolve_wallet_address(
|
|
150
|
+
wallet_label=wallet_label, wallet_address=wallet_address
|
|
151
|
+
)
|
|
152
|
+
if not addr:
|
|
153
|
+
return err(
|
|
154
|
+
"invalid_request",
|
|
155
|
+
"wallet_label or wallet_address is required",
|
|
156
|
+
{"wallet_label": wallet_label, "wallet_address": wallet_address},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if action == "user_state":
|
|
160
|
+
success, data = await adapter.get_user_state(addr)
|
|
161
|
+
return ok(
|
|
162
|
+
{
|
|
163
|
+
"wallet_address": addr,
|
|
164
|
+
"action": action,
|
|
165
|
+
"data": data,
|
|
166
|
+
"success": success,
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if action == "spot_user_state":
|
|
171
|
+
success, data = await adapter.get_spot_user_state(addr)
|
|
172
|
+
return ok(
|
|
173
|
+
{
|
|
174
|
+
"wallet_address": addr,
|
|
175
|
+
"action": action,
|
|
176
|
+
"data": data,
|
|
177
|
+
"success": success,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if action == "wait_for_deposit":
|
|
182
|
+
if expected_increase is None:
|
|
183
|
+
return err(
|
|
184
|
+
"invalid_request",
|
|
185
|
+
"expected_increase is required for wait_for_deposit",
|
|
186
|
+
{"expected_increase": expected_increase},
|
|
187
|
+
)
|
|
188
|
+
try:
|
|
189
|
+
inc = float(expected_increase)
|
|
190
|
+
except (TypeError, ValueError):
|
|
191
|
+
return err("invalid_request", "expected_increase must be a number")
|
|
192
|
+
if inc <= 0:
|
|
193
|
+
return err("invalid_request", "expected_increase must be positive")
|
|
194
|
+
|
|
195
|
+
ok_dep, final_bal = await adapter.wait_for_deposit(
|
|
196
|
+
addr,
|
|
197
|
+
inc,
|
|
198
|
+
timeout_s=int(timeout_s),
|
|
199
|
+
poll_interval_s=int(poll_interval_s),
|
|
200
|
+
)
|
|
201
|
+
return ok(
|
|
202
|
+
{
|
|
203
|
+
"wallet_address": addr,
|
|
204
|
+
"action": action,
|
|
205
|
+
"expected_increase": inc,
|
|
206
|
+
"confirmed": bool(ok_dep),
|
|
207
|
+
"final_balance_usd": float(final_bal),
|
|
208
|
+
"timeout_s": int(timeout_s),
|
|
209
|
+
"poll_interval_s": int(poll_interval_s),
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# wait_for_withdrawal
|
|
214
|
+
ok_wd, withdrawals = await adapter.wait_for_withdrawal(
|
|
215
|
+
addr,
|
|
216
|
+
lookback_s=int(lookback_s),
|
|
217
|
+
max_poll_time_s=int(max_poll_time_s),
|
|
218
|
+
poll_interval_s=int(poll_interval_s),
|
|
219
|
+
)
|
|
220
|
+
return ok(
|
|
221
|
+
{
|
|
222
|
+
"wallet_address": addr,
|
|
223
|
+
"action": action,
|
|
224
|
+
"confirmed": bool(ok_wd),
|
|
225
|
+
"withdrawals": withdrawals,
|
|
226
|
+
"lookback_s": int(lookback_s),
|
|
227
|
+
"max_poll_time_s": int(max_poll_time_s),
|
|
228
|
+
"poll_interval_s": int(poll_interval_s),
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if action == "mid_prices":
|
|
233
|
+
success, data = await adapter.get_all_mid_prices()
|
|
234
|
+
if not coin:
|
|
235
|
+
return ok({"action": action, "data": data, "success": success})
|
|
236
|
+
|
|
237
|
+
want = _PERP_SUFFIX_RE.sub("", str(coin).strip()).strip()
|
|
238
|
+
if not want:
|
|
239
|
+
return err("invalid_request", "coin is required for mid_prices filter")
|
|
240
|
+
|
|
241
|
+
price = None
|
|
242
|
+
if isinstance(data, dict):
|
|
243
|
+
for k, v in data.items():
|
|
244
|
+
if str(k).lower() == want.lower():
|
|
245
|
+
try:
|
|
246
|
+
price = float(v)
|
|
247
|
+
except (TypeError, ValueError):
|
|
248
|
+
price = None
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
return ok(
|
|
252
|
+
{
|
|
253
|
+
"action": action,
|
|
254
|
+
"coin": want,
|
|
255
|
+
"price": price,
|
|
256
|
+
"success": bool(success and price is not None),
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if action == "meta_and_asset_ctxs":
|
|
261
|
+
success, data = await adapter.get_meta_and_asset_ctxs()
|
|
262
|
+
return ok({"action": action, "data": data, "success": success})
|
|
263
|
+
|
|
264
|
+
if action == "spot_assets":
|
|
265
|
+
success, data = await adapter.get_spot_assets()
|
|
266
|
+
return ok({"action": action, "data": data, "success": success})
|
|
267
|
+
|
|
268
|
+
if action == "l2_book":
|
|
269
|
+
c = (coin or "").strip()
|
|
270
|
+
if not c:
|
|
271
|
+
return err("invalid_request", "coin is required for l2_book")
|
|
272
|
+
success, data = await adapter.get_l2_book(c, n_levels=int(n_levels))
|
|
273
|
+
return ok({"action": action, "coin": c, "data": data, "success": success})
|
|
274
|
+
|
|
275
|
+
return err("invalid_request", f"Unknown hyperliquid action: {action}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _annotate_hl_profile(
|
|
279
|
+
*,
|
|
280
|
+
address: str,
|
|
281
|
+
label: str,
|
|
282
|
+
action: str,
|
|
283
|
+
status: str,
|
|
284
|
+
details: dict[str, Any] | None = None,
|
|
285
|
+
idempotency_key: str | None = None,
|
|
286
|
+
) -> None:
|
|
287
|
+
store = WalletProfileStore.default()
|
|
288
|
+
store.annotate_safe(
|
|
289
|
+
address=address,
|
|
290
|
+
label=label,
|
|
291
|
+
protocol="hyperliquid",
|
|
292
|
+
action=action,
|
|
293
|
+
tool="hyperliquid_execute",
|
|
294
|
+
status=status,
|
|
295
|
+
chain_id=999, # Hyperliquid chain ID
|
|
296
|
+
details=details,
|
|
297
|
+
idempotency_key=idempotency_key,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def hyperliquid_execute(
|
|
302
|
+
action: Literal["place_order", "cancel_order", "update_leverage", "withdraw"],
|
|
303
|
+
*,
|
|
304
|
+
wallet_label: str,
|
|
305
|
+
dry_run: bool | None = None,
|
|
306
|
+
coin: str | None = None,
|
|
307
|
+
asset_id: int | None = None,
|
|
308
|
+
order_type: Literal["market", "limit"] = "market",
|
|
309
|
+
is_buy: bool | None = None,
|
|
310
|
+
size: float | None = None,
|
|
311
|
+
usd_amount: float | None = None,
|
|
312
|
+
usd_amount_kind: Literal["notional", "margin"] | None = None,
|
|
313
|
+
price: float | None = None,
|
|
314
|
+
slippage: float = 0.01,
|
|
315
|
+
reduce_only: bool = False,
|
|
316
|
+
cloid: str | None = None,
|
|
317
|
+
order_id: int | None = None,
|
|
318
|
+
cancel_cloid: str | None = None,
|
|
319
|
+
leverage: int | None = None,
|
|
320
|
+
is_cross: bool = True,
|
|
321
|
+
amount_usdc: float | None = None,
|
|
322
|
+
builder_fee_tenths_bp: int | None = None,
|
|
323
|
+
idempotency_key: str | None = None,
|
|
324
|
+
force: bool = False,
|
|
325
|
+
) -> dict[str, Any]:
|
|
326
|
+
want = str(wallet_label or "").strip()
|
|
327
|
+
if not want:
|
|
328
|
+
return err("invalid_request", "wallet_label is required")
|
|
329
|
+
|
|
330
|
+
store = IdempotencyStore.default()
|
|
331
|
+
key_input = {
|
|
332
|
+
"action": action,
|
|
333
|
+
"wallet_label": want,
|
|
334
|
+
"dry_run": dry_run,
|
|
335
|
+
"coin": coin,
|
|
336
|
+
"asset_id": asset_id,
|
|
337
|
+
"order_type": order_type,
|
|
338
|
+
"is_buy": is_buy,
|
|
339
|
+
"size": size,
|
|
340
|
+
"usd_amount": usd_amount,
|
|
341
|
+
"usd_amount_kind": usd_amount_kind,
|
|
342
|
+
"price": price,
|
|
343
|
+
"slippage": slippage,
|
|
344
|
+
"reduce_only": reduce_only,
|
|
345
|
+
"cloid": cloid,
|
|
346
|
+
"order_id": order_id,
|
|
347
|
+
"cancel_cloid": cancel_cloid,
|
|
348
|
+
"leverage": leverage,
|
|
349
|
+
"is_cross": is_cross,
|
|
350
|
+
"amount_usdc": amount_usdc,
|
|
351
|
+
"builder_fee_tenths_bp": builder_fee_tenths_bp,
|
|
352
|
+
}
|
|
353
|
+
tool_input = {"request": key_input, "idempotency_key": idempotency_key}
|
|
354
|
+
preview_obj = build_hyperliquid_execute_preview(tool_input)
|
|
355
|
+
preview_text = str(preview_obj.get("summary") or "").strip()
|
|
356
|
+
|
|
357
|
+
key = (idempotency_key or "").strip() or sha256_json(tool_input.get("request"))
|
|
358
|
+
prior = store.get(key)
|
|
359
|
+
if prior and not force:
|
|
360
|
+
try:
|
|
361
|
+
prior_result = prior.get("result") if isinstance(prior, dict) else None
|
|
362
|
+
if isinstance(prior_result, dict):
|
|
363
|
+
prior_result["status"] = "duplicate"
|
|
364
|
+
prior_result["idempotency_key"] = key
|
|
365
|
+
prior_result.setdefault("preview", preview_text)
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
return prior
|
|
369
|
+
|
|
370
|
+
w = find_wallet_by_label(want)
|
|
371
|
+
if not w:
|
|
372
|
+
response = err("not_found", f"Unknown wallet_label: {want}")
|
|
373
|
+
store.put(key, tool_input, response)
|
|
374
|
+
return response
|
|
375
|
+
|
|
376
|
+
sender = normalize_address(w.get("address"))
|
|
377
|
+
pk = (
|
|
378
|
+
(w.get("private_key") or w.get("private_key_hex"))
|
|
379
|
+
if isinstance(w, dict)
|
|
380
|
+
else None
|
|
381
|
+
)
|
|
382
|
+
if not sender or not pk:
|
|
383
|
+
response = err(
|
|
384
|
+
"invalid_wallet",
|
|
385
|
+
"Wallet must include address and private_key_hex in wallets.json (local dev only)",
|
|
386
|
+
{"wallet_label": want},
|
|
387
|
+
)
|
|
388
|
+
store.put(key, tool_input, response)
|
|
389
|
+
return response
|
|
390
|
+
|
|
391
|
+
dry = (
|
|
392
|
+
os.getenv("DRY_RUN", "").lower() in ("1", "true")
|
|
393
|
+
if dry_run is None
|
|
394
|
+
else bool(dry_run)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
cfg_json = load_config_json()
|
|
398
|
+
strategy_cfg = (
|
|
399
|
+
cfg_json.get("strategy") if isinstance(cfg_json.get("strategy"), dict) else {}
|
|
400
|
+
)
|
|
401
|
+
config: dict[str, Any] = dict(strategy_cfg)
|
|
402
|
+
config["main_wallet"] = {"address": sender, "private_key_hex": pk}
|
|
403
|
+
config["strategy_wallet"] = {"address": sender, "private_key_hex": pk}
|
|
404
|
+
|
|
405
|
+
effects: list[dict[str, Any]] = []
|
|
406
|
+
|
|
407
|
+
executor = None
|
|
408
|
+
if not dry:
|
|
409
|
+
try:
|
|
410
|
+
executor = LocalHyperliquidExecutor(config=config, network="mainnet")
|
|
411
|
+
except Exception as exc: # noqa: BLE001
|
|
412
|
+
response = err("executor_error", str(exc))
|
|
413
|
+
store.put(key, tool_input, response)
|
|
414
|
+
return response
|
|
415
|
+
|
|
416
|
+
adapter = HyperliquidAdapter(config=config, simulation=dry, executor=executor)
|
|
417
|
+
|
|
418
|
+
if action == "withdraw":
|
|
419
|
+
if amount_usdc is None:
|
|
420
|
+
response = err("invalid_request", "amount_usdc is required for withdraw")
|
|
421
|
+
store.put(key, tool_input, response)
|
|
422
|
+
return response
|
|
423
|
+
try:
|
|
424
|
+
amt = float(amount_usdc)
|
|
425
|
+
except (TypeError, ValueError):
|
|
426
|
+
response = err("invalid_request", "amount_usdc must be a number")
|
|
427
|
+
store.put(key, tool_input, response)
|
|
428
|
+
return response
|
|
429
|
+
if amt <= 0:
|
|
430
|
+
response = err("invalid_request", "amount_usdc must be positive")
|
|
431
|
+
store.put(key, tool_input, response)
|
|
432
|
+
return response
|
|
433
|
+
|
|
434
|
+
ok_wd, res = await adapter.withdraw(amount=amt, address=sender)
|
|
435
|
+
effects.append({"type": "hl", "label": "withdraw", "ok": ok_wd, "result": res})
|
|
436
|
+
status = "dry_run" if dry else ("confirmed" if ok_wd else "failed")
|
|
437
|
+
response = ok(
|
|
438
|
+
{
|
|
439
|
+
"idempotency_key": key,
|
|
440
|
+
"status": status,
|
|
441
|
+
"dry_run": dry,
|
|
442
|
+
"action": action,
|
|
443
|
+
"wallet_label": want,
|
|
444
|
+
"address": sender,
|
|
445
|
+
"amount_usdc": amt,
|
|
446
|
+
"preview": preview_text,
|
|
447
|
+
"effects": effects,
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
store.put(key, tool_input, response)
|
|
451
|
+
_annotate_hl_profile(
|
|
452
|
+
address=sender,
|
|
453
|
+
label=want,
|
|
454
|
+
action="withdraw",
|
|
455
|
+
status=status,
|
|
456
|
+
details={"amount_usdc": amt},
|
|
457
|
+
idempotency_key=key,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return response
|
|
461
|
+
|
|
462
|
+
def _coin_from_asset_id(aid: int) -> str | None:
|
|
463
|
+
for k, v in (adapter.coin_to_asset or {}).items():
|
|
464
|
+
try:
|
|
465
|
+
if int(v) == int(aid):
|
|
466
|
+
return str(k)
|
|
467
|
+
except Exception:
|
|
468
|
+
continue
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
ok_aid, aid_or_err = _resolve_perp_asset_id(adapter, coin=coin, asset_id=asset_id)
|
|
472
|
+
if not ok_aid:
|
|
473
|
+
payload = aid_or_err if isinstance(aid_or_err, dict) else {}
|
|
474
|
+
response = err(
|
|
475
|
+
payload.get("code") or "invalid_request",
|
|
476
|
+
payload.get("message") or "Invalid asset",
|
|
477
|
+
payload.get("details"),
|
|
478
|
+
)
|
|
479
|
+
store.put(key, tool_input, response)
|
|
480
|
+
return response
|
|
481
|
+
resolved_asset_id = int(aid_or_err)
|
|
482
|
+
|
|
483
|
+
if action == "update_leverage":
|
|
484
|
+
if leverage is None:
|
|
485
|
+
response = err(
|
|
486
|
+
"invalid_request", "leverage is required for update_leverage"
|
|
487
|
+
)
|
|
488
|
+
store.put(key, tool_input, response)
|
|
489
|
+
return response
|
|
490
|
+
try:
|
|
491
|
+
lev = int(leverage)
|
|
492
|
+
except (TypeError, ValueError):
|
|
493
|
+
response = err("invalid_request", "leverage must be an int")
|
|
494
|
+
store.put(key, tool_input, response)
|
|
495
|
+
return response
|
|
496
|
+
if lev <= 0:
|
|
497
|
+
response = err("invalid_request", "leverage must be positive")
|
|
498
|
+
store.put(key, tool_input, response)
|
|
499
|
+
return response
|
|
500
|
+
|
|
501
|
+
ok_lev, res = await adapter.update_leverage(
|
|
502
|
+
resolved_asset_id, lev, bool(is_cross), sender
|
|
503
|
+
)
|
|
504
|
+
effects.append(
|
|
505
|
+
{"type": "hl", "label": "update_leverage", "ok": ok_lev, "result": res}
|
|
506
|
+
)
|
|
507
|
+
status = "dry_run" if dry else ("confirmed" if ok_lev else "failed")
|
|
508
|
+
response = ok(
|
|
509
|
+
{
|
|
510
|
+
"idempotency_key": key,
|
|
511
|
+
"status": status,
|
|
512
|
+
"dry_run": dry,
|
|
513
|
+
"action": action,
|
|
514
|
+
"wallet_label": want,
|
|
515
|
+
"address": sender,
|
|
516
|
+
"asset_id": resolved_asset_id,
|
|
517
|
+
"coin": coin,
|
|
518
|
+
"preview": preview_text,
|
|
519
|
+
"effects": effects,
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
store.put(key, tool_input, response)
|
|
523
|
+
_annotate_hl_profile(
|
|
524
|
+
address=sender,
|
|
525
|
+
label=want,
|
|
526
|
+
action="update_leverage",
|
|
527
|
+
status=status,
|
|
528
|
+
details={"asset_id": resolved_asset_id, "coin": coin, "leverage": lev},
|
|
529
|
+
idempotency_key=key,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
return response
|
|
533
|
+
|
|
534
|
+
if action == "cancel_order":
|
|
535
|
+
if cancel_cloid:
|
|
536
|
+
ok_cancel, res = await adapter.cancel_order_by_cloid(
|
|
537
|
+
resolved_asset_id, str(cancel_cloid), sender
|
|
538
|
+
)
|
|
539
|
+
effects.append(
|
|
540
|
+
{
|
|
541
|
+
"type": "hl",
|
|
542
|
+
"label": "cancel_order_by_cloid",
|
|
543
|
+
"ok": ok_cancel,
|
|
544
|
+
"result": res,
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
else:
|
|
548
|
+
if order_id is None:
|
|
549
|
+
response = err(
|
|
550
|
+
"invalid_request",
|
|
551
|
+
"order_id or cancel_cloid is required for cancel_order",
|
|
552
|
+
)
|
|
553
|
+
store.put(key, tool_input, response)
|
|
554
|
+
return response
|
|
555
|
+
ok_cancel, res = await adapter.cancel_order(
|
|
556
|
+
resolved_asset_id, int(order_id), sender
|
|
557
|
+
)
|
|
558
|
+
effects.append(
|
|
559
|
+
{"type": "hl", "label": "cancel_order", "ok": ok_cancel, "result": res}
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
ok_all = all(bool(e.get("ok")) for e in effects) if effects else False
|
|
563
|
+
status = "dry_run" if dry else ("confirmed" if ok_all else "failed")
|
|
564
|
+
response = ok(
|
|
565
|
+
{
|
|
566
|
+
"idempotency_key": key,
|
|
567
|
+
"status": status,
|
|
568
|
+
"dry_run": dry,
|
|
569
|
+
"action": action,
|
|
570
|
+
"wallet_label": want,
|
|
571
|
+
"address": sender,
|
|
572
|
+
"asset_id": resolved_asset_id,
|
|
573
|
+
"coin": coin,
|
|
574
|
+
"preview": preview_text,
|
|
575
|
+
"effects": effects,
|
|
576
|
+
}
|
|
577
|
+
)
|
|
578
|
+
store.put(key, tool_input, response)
|
|
579
|
+
_annotate_hl_profile(
|
|
580
|
+
address=sender,
|
|
581
|
+
label=want,
|
|
582
|
+
action="cancel_order",
|
|
583
|
+
status=status,
|
|
584
|
+
details={
|
|
585
|
+
"asset_id": resolved_asset_id,
|
|
586
|
+
"coin": coin,
|
|
587
|
+
"order_id": order_id,
|
|
588
|
+
"cancel_cloid": cancel_cloid,
|
|
589
|
+
},
|
|
590
|
+
idempotency_key=key,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
return response
|
|
594
|
+
|
|
595
|
+
if size is not None and usd_amount is not None:
|
|
596
|
+
response = err(
|
|
597
|
+
"invalid_request",
|
|
598
|
+
"Provide either size (coin units) or usd_amount (USD notional/margin), not both",
|
|
599
|
+
)
|
|
600
|
+
store.put(key, tool_input, response)
|
|
601
|
+
return response
|
|
602
|
+
if usd_amount_kind is not None and usd_amount is None:
|
|
603
|
+
response = err(
|
|
604
|
+
"invalid_request",
|
|
605
|
+
"usd_amount_kind is only valid when usd_amount is provided",
|
|
606
|
+
)
|
|
607
|
+
store.put(key, tool_input, response)
|
|
608
|
+
return response
|
|
609
|
+
|
|
610
|
+
if is_buy is None:
|
|
611
|
+
response = err("invalid_request", "is_buy is required for place_order")
|
|
612
|
+
store.put(key, tool_input, response)
|
|
613
|
+
return response
|
|
614
|
+
|
|
615
|
+
if order_type == "limit":
|
|
616
|
+
if price is None:
|
|
617
|
+
response = err("invalid_request", "price is required for limit orders")
|
|
618
|
+
store.put(key, tool_input, response)
|
|
619
|
+
return response
|
|
620
|
+
try:
|
|
621
|
+
px_for_sizing = float(price)
|
|
622
|
+
except (TypeError, ValueError):
|
|
623
|
+
response = err("invalid_request", "price must be a number")
|
|
624
|
+
store.put(key, tool_input, response)
|
|
625
|
+
return response
|
|
626
|
+
if px_for_sizing <= 0:
|
|
627
|
+
response = err("invalid_request", "price must be positive")
|
|
628
|
+
store.put(key, tool_input, response)
|
|
629
|
+
return response
|
|
630
|
+
else:
|
|
631
|
+
try:
|
|
632
|
+
slip = float(slippage)
|
|
633
|
+
except (TypeError, ValueError):
|
|
634
|
+
response = err("invalid_request", "slippage must be a number")
|
|
635
|
+
store.put(key, tool_input, response)
|
|
636
|
+
return response
|
|
637
|
+
if slip < 0:
|
|
638
|
+
response = err("invalid_request", "slippage must be >= 0")
|
|
639
|
+
store.put(key, tool_input, response)
|
|
640
|
+
return response
|
|
641
|
+
if slip > 0.25:
|
|
642
|
+
response = err("invalid_request", "slippage > 0.25 is too risky")
|
|
643
|
+
store.put(key, tool_input, response)
|
|
644
|
+
return response
|
|
645
|
+
px_for_sizing = None
|
|
646
|
+
|
|
647
|
+
sizing: dict[str, Any] = {"source": "size"}
|
|
648
|
+
if size is not None:
|
|
649
|
+
try:
|
|
650
|
+
sz = float(size)
|
|
651
|
+
except (TypeError, ValueError):
|
|
652
|
+
response = err("invalid_request", "size must be a number")
|
|
653
|
+
store.put(key, tool_input, response)
|
|
654
|
+
return response
|
|
655
|
+
if sz <= 0:
|
|
656
|
+
response = err("invalid_request", "size must be positive")
|
|
657
|
+
store.put(key, tool_input, response)
|
|
658
|
+
return response
|
|
659
|
+
else:
|
|
660
|
+
if usd_amount is None:
|
|
661
|
+
response = err(
|
|
662
|
+
"invalid_request",
|
|
663
|
+
"Provide either size (coin units) or usd_amount (USD notional/margin) for place_order",
|
|
664
|
+
)
|
|
665
|
+
store.put(key, tool_input, response)
|
|
666
|
+
return response
|
|
667
|
+
if usd_amount_kind is None:
|
|
668
|
+
response = err(
|
|
669
|
+
"invalid_request",
|
|
670
|
+
"usd_amount_kind is required when providing usd_amount: choose 'notional' (position USD) or 'margin' (collateral USD; notional = margin * leverage)",
|
|
671
|
+
)
|
|
672
|
+
store.put(key, tool_input, response)
|
|
673
|
+
return response
|
|
674
|
+
try:
|
|
675
|
+
usd_amt = float(usd_amount)
|
|
676
|
+
except (TypeError, ValueError):
|
|
677
|
+
response = err("invalid_request", "usd_amount must be a number")
|
|
678
|
+
store.put(key, tool_input, response)
|
|
679
|
+
return response
|
|
680
|
+
if usd_amt <= 0:
|
|
681
|
+
response = err("invalid_request", "usd_amount must be positive")
|
|
682
|
+
store.put(key, tool_input, response)
|
|
683
|
+
return response
|
|
684
|
+
|
|
685
|
+
if usd_amount_kind == "margin":
|
|
686
|
+
if leverage is None:
|
|
687
|
+
response = err(
|
|
688
|
+
"invalid_request",
|
|
689
|
+
"leverage is required when usd_amount_kind='margin' (notional = margin * leverage)",
|
|
690
|
+
)
|
|
691
|
+
store.put(key, tool_input, response)
|
|
692
|
+
return response
|
|
693
|
+
try:
|
|
694
|
+
lev = int(leverage)
|
|
695
|
+
except (TypeError, ValueError):
|
|
696
|
+
response = err("invalid_request", "leverage must be an int")
|
|
697
|
+
store.put(key, tool_input, response)
|
|
698
|
+
return response
|
|
699
|
+
if lev <= 0:
|
|
700
|
+
response = err("invalid_request", "leverage must be positive")
|
|
701
|
+
store.put(key, tool_input, response)
|
|
702
|
+
return response
|
|
703
|
+
notional_usd = usd_amt * float(lev)
|
|
704
|
+
margin_usd = usd_amt
|
|
705
|
+
else:
|
|
706
|
+
notional_usd = usd_amt
|
|
707
|
+
margin_usd = None
|
|
708
|
+
if leverage is not None:
|
|
709
|
+
try:
|
|
710
|
+
lev = int(leverage)
|
|
711
|
+
if lev > 0:
|
|
712
|
+
margin_usd = notional_usd / float(lev)
|
|
713
|
+
except Exception:
|
|
714
|
+
margin_usd = None
|
|
715
|
+
|
|
716
|
+
if px_for_sizing is None:
|
|
717
|
+
coin_name = _PERP_SUFFIX_RE.sub("", str(coin or "").strip()).strip()
|
|
718
|
+
if not coin_name:
|
|
719
|
+
coin_name = _coin_from_asset_id(resolved_asset_id) or ""
|
|
720
|
+
if not coin_name:
|
|
721
|
+
response = err(
|
|
722
|
+
"invalid_request",
|
|
723
|
+
"coin is required when computing size from usd_amount for market orders",
|
|
724
|
+
)
|
|
725
|
+
store.put(key, tool_input, response)
|
|
726
|
+
return response
|
|
727
|
+
ok_mids, mids = await adapter.get_all_mid_prices()
|
|
728
|
+
if not ok_mids or not isinstance(mids, dict):
|
|
729
|
+
response = err("price_error", "Failed to fetch mid prices")
|
|
730
|
+
store.put(key, tool_input, response)
|
|
731
|
+
return response
|
|
732
|
+
mid = None
|
|
733
|
+
for k, v in mids.items():
|
|
734
|
+
if str(k).lower() == coin_name.lower():
|
|
735
|
+
try:
|
|
736
|
+
mid = float(v)
|
|
737
|
+
except (TypeError, ValueError):
|
|
738
|
+
mid = None
|
|
739
|
+
break
|
|
740
|
+
if mid is None or mid <= 0:
|
|
741
|
+
response = err(
|
|
742
|
+
"price_error",
|
|
743
|
+
f"Could not resolve mid price for {coin_name}",
|
|
744
|
+
)
|
|
745
|
+
store.put(key, tool_input, response)
|
|
746
|
+
return response
|
|
747
|
+
px_for_sizing = mid
|
|
748
|
+
|
|
749
|
+
sz = notional_usd / float(px_for_sizing)
|
|
750
|
+
sizing = {
|
|
751
|
+
"source": "usd_amount",
|
|
752
|
+
"usd_amount": float(usd_amt),
|
|
753
|
+
"usd_amount_kind": usd_amount_kind,
|
|
754
|
+
"notional_usd": float(notional_usd),
|
|
755
|
+
"margin_usd_estimate": float(margin_usd)
|
|
756
|
+
if margin_usd is not None
|
|
757
|
+
else None,
|
|
758
|
+
"price_used": float(px_for_sizing),
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
sz_valid = adapter.get_valid_order_size(resolved_asset_id, sz)
|
|
762
|
+
if sz_valid <= 0:
|
|
763
|
+
response = err("invalid_request", "size is too small after lot-size rounding")
|
|
764
|
+
store.put(key, tool_input, response)
|
|
765
|
+
return response
|
|
766
|
+
|
|
767
|
+
try:
|
|
768
|
+
builder = _resolve_builder_fee(
|
|
769
|
+
config=config,
|
|
770
|
+
builder_fee_tenths_bp=builder_fee_tenths_bp,
|
|
771
|
+
)
|
|
772
|
+
except ValueError as exc:
|
|
773
|
+
response = err("invalid_request", str(exc))
|
|
774
|
+
store.put(key, tool_input, response)
|
|
775
|
+
return response
|
|
776
|
+
|
|
777
|
+
if leverage is not None:
|
|
778
|
+
try:
|
|
779
|
+
lev = int(leverage)
|
|
780
|
+
except (TypeError, ValueError):
|
|
781
|
+
response = err("invalid_request", "leverage must be an int")
|
|
782
|
+
store.put(key, tool_input, response)
|
|
783
|
+
return response
|
|
784
|
+
if lev <= 0:
|
|
785
|
+
response = err("invalid_request", "leverage must be positive")
|
|
786
|
+
store.put(key, tool_input, response)
|
|
787
|
+
return response
|
|
788
|
+
ok_lev, res = await adapter.update_leverage(
|
|
789
|
+
resolved_asset_id, lev, bool(is_cross), sender
|
|
790
|
+
)
|
|
791
|
+
effects.append(
|
|
792
|
+
{"type": "hl", "label": "update_leverage", "ok": ok_lev, "result": res}
|
|
793
|
+
)
|
|
794
|
+
if not ok_lev and not dry:
|
|
795
|
+
response = ok(
|
|
796
|
+
{
|
|
797
|
+
"idempotency_key": key,
|
|
798
|
+
"status": "failed",
|
|
799
|
+
"dry_run": dry,
|
|
800
|
+
"action": action,
|
|
801
|
+
"wallet_label": want,
|
|
802
|
+
"address": sender,
|
|
803
|
+
"asset_id": resolved_asset_id,
|
|
804
|
+
"coin": coin,
|
|
805
|
+
"preview": preview_text,
|
|
806
|
+
"effects": effects,
|
|
807
|
+
}
|
|
808
|
+
)
|
|
809
|
+
store.put(key, tool_input, response)
|
|
810
|
+
return response
|
|
811
|
+
|
|
812
|
+
# Builder attribution is mandatory; ensure approval before placing orders.
|
|
813
|
+
desired = int(builder.get("f") or 0)
|
|
814
|
+
builder_addr = str(builder.get("b") or "").strip()
|
|
815
|
+
ok_fee, current = await adapter.get_max_builder_fee(
|
|
816
|
+
user=sender, builder=builder_addr
|
|
817
|
+
)
|
|
818
|
+
effects.append(
|
|
819
|
+
{
|
|
820
|
+
"type": "hl",
|
|
821
|
+
"label": "get_max_builder_fee",
|
|
822
|
+
"ok": ok_fee,
|
|
823
|
+
"result": {"current_tenths_bp": int(current), "desired_tenths_bp": desired},
|
|
824
|
+
}
|
|
825
|
+
)
|
|
826
|
+
if not ok_fee or int(current) < desired:
|
|
827
|
+
max_fee_rate = f"{desired / 1000:.3f}%"
|
|
828
|
+
ok_appr, appr = await adapter.approve_builder_fee(
|
|
829
|
+
builder=builder_addr,
|
|
830
|
+
max_fee_rate=max_fee_rate,
|
|
831
|
+
address=sender,
|
|
832
|
+
)
|
|
833
|
+
effects.append(
|
|
834
|
+
{
|
|
835
|
+
"type": "hl",
|
|
836
|
+
"label": "approve_builder_fee",
|
|
837
|
+
"ok": ok_appr,
|
|
838
|
+
"result": appr,
|
|
839
|
+
}
|
|
840
|
+
)
|
|
841
|
+
if not ok_appr and not dry:
|
|
842
|
+
response = ok(
|
|
843
|
+
{
|
|
844
|
+
"idempotency_key": key,
|
|
845
|
+
"status": "failed",
|
|
846
|
+
"dry_run": dry,
|
|
847
|
+
"action": action,
|
|
848
|
+
"wallet_label": want,
|
|
849
|
+
"address": sender,
|
|
850
|
+
"asset_id": resolved_asset_id,
|
|
851
|
+
"coin": coin,
|
|
852
|
+
"preview": preview_text,
|
|
853
|
+
"effects": effects,
|
|
854
|
+
}
|
|
855
|
+
)
|
|
856
|
+
store.put(key, tool_input, response)
|
|
857
|
+
return response
|
|
858
|
+
|
|
859
|
+
if order_type == "limit":
|
|
860
|
+
ok_order, res = await adapter.place_limit_order(
|
|
861
|
+
resolved_asset_id,
|
|
862
|
+
bool(is_buy),
|
|
863
|
+
float(price),
|
|
864
|
+
float(sz_valid),
|
|
865
|
+
sender,
|
|
866
|
+
reduce_only=bool(reduce_only),
|
|
867
|
+
builder=builder,
|
|
868
|
+
)
|
|
869
|
+
effects.append(
|
|
870
|
+
{"type": "hl", "label": "place_limit_order", "ok": ok_order, "result": res}
|
|
871
|
+
)
|
|
872
|
+
else:
|
|
873
|
+
ok_order, res = await adapter.place_market_order(
|
|
874
|
+
resolved_asset_id,
|
|
875
|
+
bool(is_buy),
|
|
876
|
+
float(slippage),
|
|
877
|
+
float(sz_valid),
|
|
878
|
+
sender,
|
|
879
|
+
reduce_only=bool(reduce_only),
|
|
880
|
+
cloid=cloid,
|
|
881
|
+
builder=builder,
|
|
882
|
+
)
|
|
883
|
+
effects.append(
|
|
884
|
+
{"type": "hl", "label": "place_market_order", "ok": ok_order, "result": res}
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
ok_all = all(bool(e.get("ok")) for e in effects) if effects else False
|
|
888
|
+
status = "dry_run" if dry else ("confirmed" if ok_all else "failed")
|
|
889
|
+
response = ok(
|
|
890
|
+
{
|
|
891
|
+
"idempotency_key": key,
|
|
892
|
+
"status": status,
|
|
893
|
+
"dry_run": dry,
|
|
894
|
+
"action": action,
|
|
895
|
+
"wallet_label": want,
|
|
896
|
+
"address": sender,
|
|
897
|
+
"asset_id": resolved_asset_id,
|
|
898
|
+
"coin": coin,
|
|
899
|
+
"order": {
|
|
900
|
+
"order_type": order_type,
|
|
901
|
+
"is_buy": bool(is_buy),
|
|
902
|
+
"size_requested": float(sz),
|
|
903
|
+
"size_valid": float(sz_valid),
|
|
904
|
+
"price": float(price) if price is not None else None,
|
|
905
|
+
"slippage": float(slippage),
|
|
906
|
+
"reduce_only": bool(reduce_only),
|
|
907
|
+
"cloid": cloid,
|
|
908
|
+
"builder": builder,
|
|
909
|
+
"sizing": sizing,
|
|
910
|
+
},
|
|
911
|
+
"preview": preview_text,
|
|
912
|
+
"effects": effects,
|
|
913
|
+
}
|
|
914
|
+
)
|
|
915
|
+
store.put(key, tool_input, response)
|
|
916
|
+
_annotate_hl_profile(
|
|
917
|
+
address=sender,
|
|
918
|
+
label=want,
|
|
919
|
+
action="place_order",
|
|
920
|
+
status=status,
|
|
921
|
+
details={
|
|
922
|
+
"asset_id": resolved_asset_id,
|
|
923
|
+
"coin": coin,
|
|
924
|
+
"order_type": order_type,
|
|
925
|
+
"is_buy": bool(is_buy),
|
|
926
|
+
"size": float(sz_valid),
|
|
927
|
+
},
|
|
928
|
+
idempotency_key=key,
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
return response
|