wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.24__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/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/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.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- 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.24.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from eth_account import Account
|
|
8
|
+
from eth_utils import to_checksum_address
|
|
9
|
+
from pydantic import BaseModel, Field, ValidationError, model_validator
|
|
10
|
+
from web3 import Web3
|
|
11
|
+
|
|
12
|
+
from wayfinder_paths.core.clients.BRAPClient import BRAPClient
|
|
13
|
+
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
14
|
+
from wayfinder_paths.core.constants import ZERO_ADDRESS
|
|
15
|
+
from wayfinder_paths.core.constants.chains import CHAIN_CODE_TO_ID
|
|
16
|
+
from wayfinder_paths.core.constants.contracts import TOKENS_REQUIRING_APPROVAL_RESET
|
|
17
|
+
from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
|
|
18
|
+
from wayfinder_paths.core.constants.hyperliquid import (
|
|
19
|
+
ARBITRUM_USDC_ADDRESS,
|
|
20
|
+
ARBITRUM_USDC_TOKEN_ID,
|
|
21
|
+
HYPERLIQUID_BRIDGE_ADDRESS,
|
|
22
|
+
)
|
|
23
|
+
from wayfinder_paths.core.utils.tokens import (
|
|
24
|
+
build_approve_transaction,
|
|
25
|
+
get_token_allowance,
|
|
26
|
+
)
|
|
27
|
+
from wayfinder_paths.core.utils.transaction import send_transaction
|
|
28
|
+
from wayfinder_paths.mcp.preview import build_execution_preview
|
|
29
|
+
from wayfinder_paths.mcp.state.profile_store import WalletProfileStore
|
|
30
|
+
from wayfinder_paths.mcp.state.store import IdempotencyStore
|
|
31
|
+
from wayfinder_paths.mcp.utils import (
|
|
32
|
+
err,
|
|
33
|
+
find_wallet_by_label,
|
|
34
|
+
normalize_address,
|
|
35
|
+
ok,
|
|
36
|
+
parse_amount_to_raw,
|
|
37
|
+
sha256_json,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ExecutionRequest(BaseModel):
|
|
42
|
+
kind: Literal["swap", "send", "hyperliquid_deposit"]
|
|
43
|
+
wallet_label: str = Field(..., description="wallets.json label (e.g. main)")
|
|
44
|
+
|
|
45
|
+
# Shared
|
|
46
|
+
amount: str = Field(..., description="Human units as a string (e.g. '1000')")
|
|
47
|
+
recipient: str | None = Field(
|
|
48
|
+
default=None, description="Destination address (defaults to sender for swap)"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# swap-only
|
|
52
|
+
from_token: str | None = Field(default=None, description="Token id/address query")
|
|
53
|
+
to_token: str | None = Field(default=None, description="Token id/address query")
|
|
54
|
+
slippage_bps: int = Field(default=50, description="Slippage in bps (50 = 0.50%)")
|
|
55
|
+
deadline_seconds: int = Field(
|
|
56
|
+
default=300, description="Best-effort TTL for quoting"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# send-only
|
|
60
|
+
token: str | None = Field(
|
|
61
|
+
default=None, description="Token id/address query, or 'native'"
|
|
62
|
+
)
|
|
63
|
+
chain_id: int | None = Field(
|
|
64
|
+
default=None, description="Required when token='native'"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@model_validator(mode="after")
|
|
68
|
+
def _validate_kind(self) -> ExecutionRequest:
|
|
69
|
+
if not self.wallet_label.strip():
|
|
70
|
+
raise ValueError("wallet_label is required")
|
|
71
|
+
if self.kind == "swap":
|
|
72
|
+
if not (self.from_token and self.to_token):
|
|
73
|
+
raise ValueError("swap requires from_token and to_token")
|
|
74
|
+
if self.slippage_bps < 0:
|
|
75
|
+
raise ValueError("slippage_bps must be >= 0")
|
|
76
|
+
if self.deadline_seconds <= 0:
|
|
77
|
+
raise ValueError("deadline_seconds must be positive")
|
|
78
|
+
if self.kind == "send":
|
|
79
|
+
if not self.token:
|
|
80
|
+
raise ValueError("send requires token")
|
|
81
|
+
if not self.recipient:
|
|
82
|
+
raise ValueError("send requires recipient")
|
|
83
|
+
if str(self.token).strip().lower() == "native" and self.chain_id is None:
|
|
84
|
+
raise ValueError("send requires chain_id when token='native'")
|
|
85
|
+
if self.kind == "hyperliquid_deposit":
|
|
86
|
+
# Hard-coded Bridge2 deposit: Arbitrum USDC -> Hyperliquid bridge address.
|
|
87
|
+
# Allow callers to omit token/recipient entirely; if provided, they must match.
|
|
88
|
+
if self.recipient and _addr_lower(self.recipient) != _addr_lower(
|
|
89
|
+
HYPERLIQUID_BRIDGE_ADDRESS
|
|
90
|
+
):
|
|
91
|
+
raise ValueError("hyperliquid_deposit recipient must be bridge address")
|
|
92
|
+
if self.token and str(self.token).strip() != ARBITRUM_USDC_TOKEN_ID:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"hyperliquid_deposit token must be {ARBITRUM_USDC_TOKEN_ID}"
|
|
95
|
+
)
|
|
96
|
+
if self.chain_id is not None and int(self.chain_id) != 42161:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"hyperliquid_deposit chain_id must be 42161 (Arbitrum)"
|
|
99
|
+
)
|
|
100
|
+
try:
|
|
101
|
+
amt = float(self.amount)
|
|
102
|
+
except (TypeError, ValueError):
|
|
103
|
+
amt = None
|
|
104
|
+
if amt is not None and amt < 5:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"hyperliquid_deposit amount must be >= 5 USDC (deposits below are lost)"
|
|
107
|
+
)
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _slippage_float(bps: int) -> float:
|
|
112
|
+
return max(0.0, float(int(bps)) / 10_000.0)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _addr_lower(addr: str | None) -> str | None:
|
|
116
|
+
if not addr:
|
|
117
|
+
return None
|
|
118
|
+
a = str(addr).strip()
|
|
119
|
+
return a.lower() if a else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _chain_id_from_token(meta: dict[str, Any]) -> int | None:
|
|
123
|
+
# Token payloads often include an internal/db `id` field. Only accept:
|
|
124
|
+
# - meta["chain_id"] (preferred)
|
|
125
|
+
# - meta["chain"]["chain_id"] / ["chainId"] / ["id"]
|
|
126
|
+
if meta.get("chain_id") is not None:
|
|
127
|
+
try:
|
|
128
|
+
return int(meta.get("chain_id"))
|
|
129
|
+
except (TypeError, ValueError):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
chain = meta.get("chain") or {}
|
|
133
|
+
if not isinstance(chain, dict):
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
for key in ("chain_id", "chainId", "id"):
|
|
137
|
+
if chain.get(key) is None:
|
|
138
|
+
continue
|
|
139
|
+
try:
|
|
140
|
+
return int(chain.get(key))
|
|
141
|
+
except (TypeError, ValueError):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
_SIMPLE_CHAIN_SUFFIX_RE = re.compile(r"^[a-z0-9]+\s+[a-z0-9-]+$", re.IGNORECASE)
|
|
148
|
+
_ASSET_CHAIN_SPLIT_RE = re.compile(
|
|
149
|
+
r"^(?P<asset>[a-z0-9]+)[- _](?P<chain>[a-z0-9-]+)$", re.IGNORECASE
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _normalize_token_query(query: str) -> str:
|
|
154
|
+
q = " ".join(str(query).strip().split())
|
|
155
|
+
if not q or "-" in q or "_" in q:
|
|
156
|
+
return q
|
|
157
|
+
if not _SIMPLE_CHAIN_SUFFIX_RE.match(q):
|
|
158
|
+
return q
|
|
159
|
+
asset, chain_code = q.rsplit(" ", 1)
|
|
160
|
+
if chain_code.lower() in CHAIN_CODE_TO_ID:
|
|
161
|
+
return f"{asset}-{chain_code}"
|
|
162
|
+
return q
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _is_eth_like_token(meta: dict[str, Any]) -> bool:
|
|
166
|
+
asset_id = str(meta.get("asset_id") or "").lower()
|
|
167
|
+
symbol = str(meta.get("symbol") or "").lower()
|
|
168
|
+
return asset_id == "ethereum" or symbol == "eth"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _split_asset_chain(query: str) -> tuple[str, str] | None:
|
|
172
|
+
q = str(query).strip()
|
|
173
|
+
if not q:
|
|
174
|
+
return None
|
|
175
|
+
m = _ASSET_CHAIN_SPLIT_RE.match(q)
|
|
176
|
+
if not m:
|
|
177
|
+
return None
|
|
178
|
+
return m.group("asset").lower(), m.group("chain").lower()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def _resolve_token_meta(
|
|
182
|
+
token_client: TokenClient,
|
|
183
|
+
*,
|
|
184
|
+
query: str,
|
|
185
|
+
) -> tuple[str, dict[str, Any]]:
|
|
186
|
+
q = _normalize_token_query(query)
|
|
187
|
+
split = _split_asset_chain(q)
|
|
188
|
+
if split:
|
|
189
|
+
asset, chain_code = split
|
|
190
|
+
if asset in {"eth", "ethereum"}:
|
|
191
|
+
try:
|
|
192
|
+
gas_meta = await token_client.get_gas_token(chain_code)
|
|
193
|
+
if isinstance(gas_meta, dict) and _is_eth_like_token(gas_meta):
|
|
194
|
+
return q, gas_meta
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
meta = await token_client.get_token_details(q)
|
|
198
|
+
return q, meta
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _infer_chain_code_from_query(query: str, meta: dict[str, Any]) -> str | None:
|
|
202
|
+
q = str(query).strip().lower()
|
|
203
|
+
if not q:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
candidates: set[str] = {str(k).lower() for k in CHAIN_CODE_TO_ID.keys()}
|
|
207
|
+
addrs = meta.get("addresses") or {}
|
|
208
|
+
if isinstance(addrs, dict):
|
|
209
|
+
candidates.update(str(k).lower() for k in addrs.keys())
|
|
210
|
+
|
|
211
|
+
best: str | None = None
|
|
212
|
+
for code in candidates:
|
|
213
|
+
if q.endswith(f"-{code}"):
|
|
214
|
+
if best is None or len(code) > len(best):
|
|
215
|
+
best = code
|
|
216
|
+
return best
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _address_for_chain(meta: dict[str, Any], chain_code: str) -> str | None:
|
|
220
|
+
addrs = meta.get("addresses") or {}
|
|
221
|
+
if not isinstance(addrs, dict):
|
|
222
|
+
return None
|
|
223
|
+
for key, val in addrs.items():
|
|
224
|
+
if str(key).lower() == chain_code and val:
|
|
225
|
+
return str(val)
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _select_token_chain(
|
|
230
|
+
meta: dict[str, Any], *, query: str
|
|
231
|
+
) -> tuple[int | None, str | None]:
|
|
232
|
+
chain_id = _chain_id_from_token(meta)
|
|
233
|
+
token_address = meta.get("address")
|
|
234
|
+
|
|
235
|
+
desired_chain = _infer_chain_code_from_query(query, meta)
|
|
236
|
+
if desired_chain:
|
|
237
|
+
addr = _address_for_chain(meta, desired_chain)
|
|
238
|
+
if addr:
|
|
239
|
+
token_address = addr
|
|
240
|
+
if desired_chain in CHAIN_CODE_TO_ID:
|
|
241
|
+
chain_id = CHAIN_CODE_TO_ID[desired_chain]
|
|
242
|
+
|
|
243
|
+
token_address_out = str(token_address).strip() if token_address else None
|
|
244
|
+
return chain_id, token_address_out
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _sanitize_for_json(obj: Any) -> Any:
|
|
248
|
+
"""Recursively convert HexBytes and bytes to hex strings for JSON serialization."""
|
|
249
|
+
if hasattr(obj, "hex") and callable(obj.hex):
|
|
250
|
+
return obj.hex()
|
|
251
|
+
if isinstance(obj, bytes):
|
|
252
|
+
return obj.hex()
|
|
253
|
+
if isinstance(obj, dict):
|
|
254
|
+
return {k: _sanitize_for_json(v) for k, v in obj.items()}
|
|
255
|
+
if isinstance(obj, (list, tuple)):
|
|
256
|
+
return [_sanitize_for_json(v) for v in obj]
|
|
257
|
+
return obj
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _is_dry_run() -> bool:
|
|
261
|
+
return os.getenv("DRY_RUN", "").lower() in ("1", "true")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _make_sign_callback(private_key: str):
|
|
265
|
+
account = Account.from_key(private_key)
|
|
266
|
+
|
|
267
|
+
async def sign_callback(transaction: dict) -> bytes:
|
|
268
|
+
signed = account.sign_transaction(transaction)
|
|
269
|
+
return signed.raw_transaction
|
|
270
|
+
|
|
271
|
+
return sign_callback
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def _broadcast(
|
|
275
|
+
sign_callback,
|
|
276
|
+
tx: dict[str, Any],
|
|
277
|
+
*,
|
|
278
|
+
chain_id: int,
|
|
279
|
+
) -> tuple[bool, dict[str, Any]]:
|
|
280
|
+
if _is_dry_run():
|
|
281
|
+
return True, {"dry_run": True, "transaction": tx, "chain_id": chain_id}
|
|
282
|
+
try:
|
|
283
|
+
tx_hash = await send_transaction(tx, sign_callback, wait_for_receipt=True)
|
|
284
|
+
return True, {"tx_hash": tx_hash, "chain_id": chain_id}
|
|
285
|
+
except Exception as e:
|
|
286
|
+
return False, {"error": _sanitize_for_json(str(e)), "chain_id": chain_id}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def _ensure_allowance(
|
|
290
|
+
*,
|
|
291
|
+
sign_callback,
|
|
292
|
+
chain_id: int,
|
|
293
|
+
token_address: str,
|
|
294
|
+
owner: str,
|
|
295
|
+
spender: str,
|
|
296
|
+
amount: int,
|
|
297
|
+
) -> tuple[bool, list[dict[str, Any]]]:
|
|
298
|
+
# May emit 0-allowance clear tx for tokens requiring approval reset (e.g. USDT)
|
|
299
|
+
effects: list[dict[str, Any]] = []
|
|
300
|
+
|
|
301
|
+
allowance = await get_token_allowance(
|
|
302
|
+
token_address=token_address,
|
|
303
|
+
chain_id=chain_id,
|
|
304
|
+
owner_address=owner,
|
|
305
|
+
spender_address=spender,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
token_checksum = to_checksum_address(token_address)
|
|
309
|
+
if (chain_id, token_checksum) in TOKENS_REQUIRING_APPROVAL_RESET and allowance > 0:
|
|
310
|
+
clear_tx = await build_approve_transaction(
|
|
311
|
+
from_address=owner,
|
|
312
|
+
chain_id=chain_id,
|
|
313
|
+
token_address=token_checksum,
|
|
314
|
+
spender_address=spender,
|
|
315
|
+
amount=0,
|
|
316
|
+
)
|
|
317
|
+
sent_ok, sent = await _broadcast(sign_callback, clear_tx, chain_id=chain_id)
|
|
318
|
+
effects.append({"type": "tx", "label": "approve_clear", **sent})
|
|
319
|
+
if not sent_ok:
|
|
320
|
+
return False, effects
|
|
321
|
+
# After clear, we will still submit a fresh approve below.
|
|
322
|
+
|
|
323
|
+
if allowance >= int(amount):
|
|
324
|
+
return True, effects
|
|
325
|
+
|
|
326
|
+
approve_tx = await build_approve_transaction(
|
|
327
|
+
from_address=owner,
|
|
328
|
+
chain_id=chain_id,
|
|
329
|
+
token_address=token_checksum,
|
|
330
|
+
spender_address=spender,
|
|
331
|
+
amount=int(amount),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
sent_ok, sent = await _broadcast(sign_callback, approve_tx, chain_id=chain_id)
|
|
335
|
+
effects.append({"type": "tx", "label": "approve", **sent})
|
|
336
|
+
return sent_ok, effects
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _annotate_profile(
|
|
340
|
+
*,
|
|
341
|
+
address: str,
|
|
342
|
+
label: str,
|
|
343
|
+
protocol: str,
|
|
344
|
+
action: str,
|
|
345
|
+
status: str,
|
|
346
|
+
chain_id: int | None = None,
|
|
347
|
+
details: dict[str, Any] | None = None,
|
|
348
|
+
idempotency_key: str | None = None,
|
|
349
|
+
) -> None:
|
|
350
|
+
store = WalletProfileStore.default()
|
|
351
|
+
store.annotate_safe(
|
|
352
|
+
address=address,
|
|
353
|
+
label=label,
|
|
354
|
+
protocol=protocol,
|
|
355
|
+
action=action,
|
|
356
|
+
tool="execute",
|
|
357
|
+
status=status,
|
|
358
|
+
chain_id=chain_id,
|
|
359
|
+
details=details,
|
|
360
|
+
idempotency_key=idempotency_key,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
async def execute(
|
|
365
|
+
*,
|
|
366
|
+
request: dict[str, Any],
|
|
367
|
+
idempotency_key: str | None = None,
|
|
368
|
+
force: bool = False,
|
|
369
|
+
) -> dict[str, Any]:
|
|
370
|
+
if isinstance(request, dict) and "kind" not in request and "action" in request:
|
|
371
|
+
# Back-compat: some callers use "action":"swap|send".
|
|
372
|
+
request = {**request, "kind": request.get("action")}
|
|
373
|
+
try:
|
|
374
|
+
req = ExecutionRequest.model_validate(request)
|
|
375
|
+
except ValidationError as exc:
|
|
376
|
+
return err("invalid_request", "execute.request validation failed", exc.errors())
|
|
377
|
+
|
|
378
|
+
store = IdempotencyStore.default()
|
|
379
|
+
|
|
380
|
+
tool_input = {
|
|
381
|
+
"request": req.model_dump(mode="json"),
|
|
382
|
+
"idempotency_key": idempotency_key,
|
|
383
|
+
}
|
|
384
|
+
preview_obj = build_execution_preview(tool_input)
|
|
385
|
+
preview_text = str(preview_obj.get("summary") or "").strip()
|
|
386
|
+
|
|
387
|
+
key = (idempotency_key or "").strip() or sha256_json(tool_input.get("request"))
|
|
388
|
+
prior = store.get(key)
|
|
389
|
+
if prior and not force:
|
|
390
|
+
# Ignore cached preflight errors (no side effects) so fixes/config changes can be retried.
|
|
391
|
+
if isinstance(prior, dict) and prior.get("ok") is False:
|
|
392
|
+
prior = None
|
|
393
|
+
else:
|
|
394
|
+
# Return the previous response, but annotate status for clarity.
|
|
395
|
+
try:
|
|
396
|
+
prior_result = prior.get("result") if isinstance(prior, dict) else None
|
|
397
|
+
if isinstance(prior_result, dict):
|
|
398
|
+
prior_result["status"] = "duplicate"
|
|
399
|
+
prior_result["idempotency_key"] = key
|
|
400
|
+
prior_result.setdefault("preview", preview_text)
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
return prior
|
|
404
|
+
|
|
405
|
+
w = find_wallet_by_label(req.wallet_label)
|
|
406
|
+
if not w:
|
|
407
|
+
response = err("not_found", f"Unknown wallet_label: {req.wallet_label}")
|
|
408
|
+
store.put(key, tool_input, response)
|
|
409
|
+
return response
|
|
410
|
+
|
|
411
|
+
sender = normalize_address(w.get("address"))
|
|
412
|
+
pk = (
|
|
413
|
+
(w.get("private_key") or w.get("private_key_hex"))
|
|
414
|
+
if isinstance(w, dict)
|
|
415
|
+
else None
|
|
416
|
+
)
|
|
417
|
+
if not sender or not pk:
|
|
418
|
+
response = err(
|
|
419
|
+
"invalid_wallet",
|
|
420
|
+
"Wallet must include address and private_key_hex in wallets.json (local dev only)",
|
|
421
|
+
{"wallet_label": req.wallet_label},
|
|
422
|
+
)
|
|
423
|
+
store.put(key, tool_input, response)
|
|
424
|
+
return response
|
|
425
|
+
|
|
426
|
+
sign_callback = _make_sign_callback(pk)
|
|
427
|
+
|
|
428
|
+
effects: list[dict[str, Any]] = []
|
|
429
|
+
|
|
430
|
+
if req.kind == "swap":
|
|
431
|
+
rcpt = normalize_address(req.recipient) or sender
|
|
432
|
+
token_client = TokenClient()
|
|
433
|
+
brap_client = BRAPClient()
|
|
434
|
+
try:
|
|
435
|
+
from_q, from_meta = await _resolve_token_meta(
|
|
436
|
+
token_client, query=str(req.from_token)
|
|
437
|
+
)
|
|
438
|
+
to_q, to_meta = await _resolve_token_meta(
|
|
439
|
+
token_client, query=str(req.to_token)
|
|
440
|
+
)
|
|
441
|
+
except Exception as exc: # noqa: BLE001
|
|
442
|
+
response = err("token_error", str(exc))
|
|
443
|
+
store.put(key, tool_input, response)
|
|
444
|
+
return response
|
|
445
|
+
|
|
446
|
+
from_chain_id, from_token_addr = _select_token_chain(from_meta, query=from_q)
|
|
447
|
+
to_chain_id, to_token_addr = _select_token_chain(to_meta, query=to_q)
|
|
448
|
+
if from_chain_id is None or to_chain_id is None:
|
|
449
|
+
response = err(
|
|
450
|
+
"invalid_token",
|
|
451
|
+
"Could not resolve chain_id for one or more tokens",
|
|
452
|
+
{"from_chain_id": from_chain_id, "to_chain_id": to_chain_id},
|
|
453
|
+
)
|
|
454
|
+
store.put(key, tool_input, response)
|
|
455
|
+
return response
|
|
456
|
+
if not from_token_addr or not to_token_addr:
|
|
457
|
+
response = err(
|
|
458
|
+
"invalid_token",
|
|
459
|
+
"Could not resolve token address for one or more tokens",
|
|
460
|
+
{
|
|
461
|
+
"from_token_address": from_token_addr,
|
|
462
|
+
"to_token_address": to_token_addr,
|
|
463
|
+
},
|
|
464
|
+
)
|
|
465
|
+
store.put(key, tool_input, response)
|
|
466
|
+
return response
|
|
467
|
+
|
|
468
|
+
decimals = int(from_meta.get("decimals") or 18)
|
|
469
|
+
try:
|
|
470
|
+
amount_raw = parse_amount_to_raw(req.amount, decimals)
|
|
471
|
+
except ValueError as exc:
|
|
472
|
+
response = err("invalid_amount", str(exc))
|
|
473
|
+
store.put(key, tool_input, response)
|
|
474
|
+
return response
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
quote_data = await brap_client.get_quote(
|
|
478
|
+
from_token=from_token_addr,
|
|
479
|
+
to_token=to_token_addr,
|
|
480
|
+
from_chain=from_chain_id,
|
|
481
|
+
to_chain=to_chain_id,
|
|
482
|
+
from_wallet=sender,
|
|
483
|
+
from_amount=str(amount_raw),
|
|
484
|
+
)
|
|
485
|
+
except Exception as exc: # noqa: BLE001
|
|
486
|
+
response = err("quote_error", str(exc))
|
|
487
|
+
store.put(key, tool_input, response)
|
|
488
|
+
return response
|
|
489
|
+
|
|
490
|
+
best_quote = (
|
|
491
|
+
quote_data.get("best_quote") if isinstance(quote_data, dict) else None
|
|
492
|
+
)
|
|
493
|
+
if not isinstance(best_quote, dict):
|
|
494
|
+
response = err(
|
|
495
|
+
"quote_error", "No best_quote returned", {"quote": quote_data}
|
|
496
|
+
)
|
|
497
|
+
store.put(key, tool_input, response)
|
|
498
|
+
return response
|
|
499
|
+
|
|
500
|
+
calldata = best_quote.get("calldata") or {}
|
|
501
|
+
if not isinstance(calldata, dict) or not calldata:
|
|
502
|
+
response = err(
|
|
503
|
+
"quote_error", "best_quote missing calldata", {"best_quote": best_quote}
|
|
504
|
+
)
|
|
505
|
+
store.put(key, tool_input, response)
|
|
506
|
+
return response
|
|
507
|
+
|
|
508
|
+
swap_tx = dict(calldata)
|
|
509
|
+
swap_tx["chainId"] = int(from_chain_id)
|
|
510
|
+
swap_tx["from"] = to_checksum_address(sender)
|
|
511
|
+
|
|
512
|
+
token_addr = from_token_addr
|
|
513
|
+
spender = swap_tx.get("to")
|
|
514
|
+
approve_amount = (
|
|
515
|
+
best_quote.get("input_amount")
|
|
516
|
+
or best_quote.get("inputAmount")
|
|
517
|
+
or best_quote.get("amount1")
|
|
518
|
+
or best_quote.get("amount")
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if (
|
|
522
|
+
token_addr
|
|
523
|
+
and isinstance(token_addr, str)
|
|
524
|
+
and token_addr.strip()
|
|
525
|
+
and token_addr.lower() != ZERO_ADDRESS.lower()
|
|
526
|
+
and spender
|
|
527
|
+
and approve_amount is not None
|
|
528
|
+
):
|
|
529
|
+
try:
|
|
530
|
+
need = int(approve_amount)
|
|
531
|
+
except Exception:
|
|
532
|
+
need = int(amount_raw)
|
|
533
|
+
ok_allow, approval_effects = await _ensure_allowance(
|
|
534
|
+
sign_callback=sign_callback,
|
|
535
|
+
chain_id=int(from_chain_id),
|
|
536
|
+
token_address=token_addr,
|
|
537
|
+
owner=to_checksum_address(sender),
|
|
538
|
+
spender=to_checksum_address(str(spender)),
|
|
539
|
+
amount=need,
|
|
540
|
+
)
|
|
541
|
+
effects.extend(approval_effects)
|
|
542
|
+
if not ok_allow:
|
|
543
|
+
response = ok(
|
|
544
|
+
{
|
|
545
|
+
"idempotency_key": key,
|
|
546
|
+
"status": "failed",
|
|
547
|
+
"dry_run": _is_dry_run(),
|
|
548
|
+
"kind": "swap",
|
|
549
|
+
"sender": sender,
|
|
550
|
+
"recipient": rcpt,
|
|
551
|
+
"preview": preview_text,
|
|
552
|
+
"effects": effects,
|
|
553
|
+
"raw": {"quote": quote_data},
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
store.put(key, tool_input, response)
|
|
557
|
+
return response
|
|
558
|
+
|
|
559
|
+
sent_ok, sent = await _broadcast(
|
|
560
|
+
sign_callback, swap_tx, chain_id=int(from_chain_id)
|
|
561
|
+
)
|
|
562
|
+
effects.append({"type": "tx", "label": "swap", **sent})
|
|
563
|
+
|
|
564
|
+
status = (
|
|
565
|
+
"confirmed"
|
|
566
|
+
if (sent_ok and not _is_dry_run())
|
|
567
|
+
else ("dry_run" if sent_ok else "failed")
|
|
568
|
+
)
|
|
569
|
+
response = ok(
|
|
570
|
+
{
|
|
571
|
+
"idempotency_key": key,
|
|
572
|
+
"status": status,
|
|
573
|
+
"dry_run": _is_dry_run(),
|
|
574
|
+
"kind": "swap",
|
|
575
|
+
"sender": sender,
|
|
576
|
+
"recipient": rcpt,
|
|
577
|
+
"preview": preview_text,
|
|
578
|
+
"effects": effects,
|
|
579
|
+
"raw": {"quote": quote_data, "best_quote": best_quote},
|
|
580
|
+
}
|
|
581
|
+
)
|
|
582
|
+
store.put(key, tool_input, response)
|
|
583
|
+
|
|
584
|
+
_annotate_profile(
|
|
585
|
+
address=sender,
|
|
586
|
+
label=req.wallet_label,
|
|
587
|
+
protocol="brap",
|
|
588
|
+
action="swap",
|
|
589
|
+
status=status,
|
|
590
|
+
chain_id=int(from_chain_id),
|
|
591
|
+
details={
|
|
592
|
+
"from_token": str(req.from_token),
|
|
593
|
+
"to_token": str(req.to_token),
|
|
594
|
+
"amount": req.amount,
|
|
595
|
+
},
|
|
596
|
+
idempotency_key=key,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
return response
|
|
600
|
+
|
|
601
|
+
is_hl_deposit = req.kind == "hyperliquid_deposit"
|
|
602
|
+
recipient = (
|
|
603
|
+
normalize_address(HYPERLIQUID_BRIDGE_ADDRESS)
|
|
604
|
+
if is_hl_deposit
|
|
605
|
+
else normalize_address(req.recipient)
|
|
606
|
+
)
|
|
607
|
+
if not recipient:
|
|
608
|
+
response = err("invalid_request", "recipient is required for send")
|
|
609
|
+
store.put(key, tool_input, response)
|
|
610
|
+
return response
|
|
611
|
+
|
|
612
|
+
token_q = ARBITRUM_USDC_TOKEN_ID if is_hl_deposit else str(req.token or "").strip()
|
|
613
|
+
token_client = TokenClient()
|
|
614
|
+
|
|
615
|
+
if token_q.lower() == "native":
|
|
616
|
+
chain_id = int(req.chain_id or 0)
|
|
617
|
+
if chain_id <= 0:
|
|
618
|
+
response = err(
|
|
619
|
+
"invalid_request", "chain_id must be provided for native sends"
|
|
620
|
+
)
|
|
621
|
+
store.put(key, tool_input, response)
|
|
622
|
+
return response
|
|
623
|
+
# Native send uses 18 decimals.
|
|
624
|
+
try:
|
|
625
|
+
value = parse_amount_to_raw(req.amount, 18)
|
|
626
|
+
except ValueError as exc:
|
|
627
|
+
response = err("invalid_amount", str(exc))
|
|
628
|
+
store.put(key, tool_input, response)
|
|
629
|
+
return response
|
|
630
|
+
|
|
631
|
+
tx = {
|
|
632
|
+
"chainId": chain_id,
|
|
633
|
+
"from": to_checksum_address(sender),
|
|
634
|
+
"to": to_checksum_address(recipient),
|
|
635
|
+
"value": int(value),
|
|
636
|
+
}
|
|
637
|
+
sent_ok, sent = await _broadcast(sign_callback, tx, chain_id=chain_id)
|
|
638
|
+
effects.append({"type": "tx", "label": "send_native", **sent})
|
|
639
|
+
status = (
|
|
640
|
+
"confirmed"
|
|
641
|
+
if (sent_ok and not _is_dry_run())
|
|
642
|
+
else ("dry_run" if sent_ok else "failed")
|
|
643
|
+
)
|
|
644
|
+
response = ok(
|
|
645
|
+
{
|
|
646
|
+
"idempotency_key": key,
|
|
647
|
+
"status": status,
|
|
648
|
+
"dry_run": _is_dry_run(),
|
|
649
|
+
"kind": req.kind,
|
|
650
|
+
"sender": sender,
|
|
651
|
+
"recipient": recipient,
|
|
652
|
+
"preview": preview_text,
|
|
653
|
+
"effects": effects,
|
|
654
|
+
"raw": {"transaction": tx},
|
|
655
|
+
}
|
|
656
|
+
)
|
|
657
|
+
store.put(key, tool_input, response)
|
|
658
|
+
_annotate_profile(
|
|
659
|
+
address=sender,
|
|
660
|
+
label=req.wallet_label,
|
|
661
|
+
protocol="balance",
|
|
662
|
+
action="send_native",
|
|
663
|
+
status=status,
|
|
664
|
+
chain_id=chain_id,
|
|
665
|
+
details={"recipient": recipient, "amount": req.amount},
|
|
666
|
+
idempotency_key=key,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
return response
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
token_meta = await token_client.get_token_details(token_q)
|
|
673
|
+
except Exception as exc: # noqa: BLE001
|
|
674
|
+
response = err("token_error", str(exc))
|
|
675
|
+
store.put(key, tool_input, response)
|
|
676
|
+
return response
|
|
677
|
+
|
|
678
|
+
token_address = str(token_meta.get("address") or "").strip()
|
|
679
|
+
chain_id = _chain_id_from_token(token_meta)
|
|
680
|
+
if not token_address or chain_id is None:
|
|
681
|
+
response = err(
|
|
682
|
+
"invalid_token", "Token missing address/chain_id", {"token": token_meta}
|
|
683
|
+
)
|
|
684
|
+
store.put(key, tool_input, response)
|
|
685
|
+
return response
|
|
686
|
+
if is_hl_deposit:
|
|
687
|
+
if int(chain_id) != 42161:
|
|
688
|
+
response = err(
|
|
689
|
+
"invalid_token",
|
|
690
|
+
"Hyperliquid deposits require Arbitrum USDC (chain_id 42161)",
|
|
691
|
+
{"token": token_meta, "chain_id": chain_id},
|
|
692
|
+
)
|
|
693
|
+
store.put(key, tool_input, response)
|
|
694
|
+
return response
|
|
695
|
+
if _addr_lower(token_address) != _addr_lower(ARBITRUM_USDC_ADDRESS):
|
|
696
|
+
response = err(
|
|
697
|
+
"invalid_token",
|
|
698
|
+
"Resolved USDC token address does not match expected Arbitrum USDC",
|
|
699
|
+
{"token": token_meta, "expected": ARBITRUM_USDC_ADDRESS},
|
|
700
|
+
)
|
|
701
|
+
store.put(key, tool_input, response)
|
|
702
|
+
return response
|
|
703
|
+
|
|
704
|
+
decimals = int(token_meta.get("decimals") or 18)
|
|
705
|
+
try:
|
|
706
|
+
amount_raw = parse_amount_to_raw(req.amount, decimals)
|
|
707
|
+
except ValueError as exc:
|
|
708
|
+
response = err("invalid_amount", str(exc))
|
|
709
|
+
store.put(key, tool_input, response)
|
|
710
|
+
return response
|
|
711
|
+
|
|
712
|
+
w3 = Web3()
|
|
713
|
+
contract = w3.eth.contract(
|
|
714
|
+
address=to_checksum_address(token_address), abi=ERC20_ABI
|
|
715
|
+
)
|
|
716
|
+
data = contract.encode_abi(
|
|
717
|
+
"transfer", args=[to_checksum_address(recipient), int(amount_raw)]
|
|
718
|
+
)
|
|
719
|
+
tx = {
|
|
720
|
+
"chainId": int(chain_id),
|
|
721
|
+
"from": to_checksum_address(sender),
|
|
722
|
+
"to": to_checksum_address(token_address),
|
|
723
|
+
"data": data,
|
|
724
|
+
"value": 0,
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
sent_ok, sent = await _broadcast(sign_callback, tx, chain_id=int(chain_id))
|
|
728
|
+
effects.append(
|
|
729
|
+
{
|
|
730
|
+
"type": "tx",
|
|
731
|
+
"label": "hyperliquid_deposit" if is_hl_deposit else "send_erc20",
|
|
732
|
+
**sent,
|
|
733
|
+
}
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
status = (
|
|
737
|
+
"confirmed"
|
|
738
|
+
if (sent_ok and not _is_dry_run())
|
|
739
|
+
else ("dry_run" if sent_ok else "failed")
|
|
740
|
+
)
|
|
741
|
+
response = ok(
|
|
742
|
+
{
|
|
743
|
+
"idempotency_key": key,
|
|
744
|
+
"status": status,
|
|
745
|
+
"dry_run": _is_dry_run(),
|
|
746
|
+
"kind": req.kind,
|
|
747
|
+
"sender": sender,
|
|
748
|
+
"recipient": recipient,
|
|
749
|
+
"preview": preview_text,
|
|
750
|
+
"effects": effects,
|
|
751
|
+
"raw": {"token": token_meta, "transaction": tx},
|
|
752
|
+
}
|
|
753
|
+
)
|
|
754
|
+
store.put(key, tool_input, response)
|
|
755
|
+
_annotate_profile(
|
|
756
|
+
address=sender,
|
|
757
|
+
label=req.wallet_label,
|
|
758
|
+
protocol="hyperliquid" if is_hl_deposit else "balance",
|
|
759
|
+
action="hyperliquid_deposit" if is_hl_deposit else "send_erc20",
|
|
760
|
+
status=status,
|
|
761
|
+
chain_id=int(chain_id),
|
|
762
|
+
details={
|
|
763
|
+
"recipient": recipient,
|
|
764
|
+
"amount": req.amount,
|
|
765
|
+
"token": token_q,
|
|
766
|
+
},
|
|
767
|
+
idempotency_key=key,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
return response
|