@agentlayer.tech/wallet 0.1.30 → 0.1.32
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.
- package/.openclaw/extensions/agent-wallet/README.md +1 -2
- package/.openclaw/extensions/agent-wallet/dist/index.js +6 -340
- package/.openclaw/extensions/agent-wallet/index.ts +6 -340
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +0 -43
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +1 -3
- package/CHANGELOG.md +35 -0
- package/README.md +0 -5
- package/agent-wallet/.env.example +0 -12
- package/agent-wallet/README.md +0 -35
- package/agent-wallet/agent_wallet/btc_user_wallets.py +32 -1
- package/agent-wallet/agent_wallet/config.py +11 -7
- package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +1 -655
- package/agent-wallet/agent_wallet/openclaw_cli.py +0 -7
- package/agent-wallet/agent_wallet/providers/evm_portfolio.py +18 -42
- package/agent-wallet/agent_wallet/providers/jupiter.py +1 -307
- package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +31 -3
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +37 -3
- package/agent-wallet/agent_wallet/transaction_policy.py +0 -262
- package/agent-wallet/agent_wallet/wallet_layer/base.py +0 -100
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +1 -1118
- package/agent-wallet/openclaw.plugin.json +0 -4
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_agent_wallet.py +113 -6
- package/agent-wallet/scripts/install_openclaw_local_config.py +7 -5
- package/agent-wallet/skills/wallet-operator/SKILL.md +1 -5
- package/bin/openclaw-agent-wallet.mjs +103 -36
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +7 -2
- package/codex/plugins/agent-wallet/server.py +2 -118
- package/hermes/plugins/agent_wallet/tools.py +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/src/local_vault.js +45 -68
- package/wdk-btc-wallet/src/server.js +1 -0
- package/wdk-evm-wallet/README.md +4 -3
- package/wdk-evm-wallet/src/config.js +15 -0
- package/wdk-evm-wallet/src/local_vault.js +45 -68
- package/wdk-evm-wallet/src/server.js +1 -0
- package/agent-wallet/agent_wallet/providers/houdini.py +0 -539
|
@@ -1,539 +0,0 @@
|
|
|
1
|
-
"""Houdini private swap provider helpers."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
import time
|
|
7
|
-
from decimal import Decimal, InvalidOperation
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from agent_wallet.config import settings
|
|
11
|
-
from agent_wallet.exceptions import ProviderError
|
|
12
|
-
from agent_wallet.http_client import get_client
|
|
13
|
-
|
|
14
|
-
_TOKEN_CACHE_TTL_SECONDS = 24 * 60 * 60
|
|
15
|
-
_CEX_TOKEN_CACHE: dict[str, tuple[float, list[dict[str, Any]]]] = {}
|
|
16
|
-
_SOLANA_NATIVE_ALIASES = {"sol", "native", "solana"}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _gateway_base_url() -> str:
|
|
20
|
-
return os.getenv("PROVIDER_GATEWAY_URL", settings.provider_gateway_url).strip().rstrip("/")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _gateway_enabled() -> bool:
|
|
24
|
-
return bool(_gateway_base_url())
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _gateway_headers() -> dict[str, str]:
|
|
28
|
-
headers = {
|
|
29
|
-
"Accept": "application/json",
|
|
30
|
-
"x-user-agent": settings.houdini_user_agent.strip() or "AgentLayer/0.1.12",
|
|
31
|
-
"x-user-timezone": settings.houdini_user_timezone.strip() or "UTC",
|
|
32
|
-
}
|
|
33
|
-
bearer = os.getenv(
|
|
34
|
-
"PROVIDER_GATEWAY_BEARER_TOKEN",
|
|
35
|
-
settings.provider_gateway_bearer_token,
|
|
36
|
-
).strip()
|
|
37
|
-
if bearer:
|
|
38
|
-
headers["Authorization"] = f"Bearer {bearer}"
|
|
39
|
-
return headers
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _base_url() -> str:
|
|
43
|
-
return settings.houdini_api_base_url.rstrip("/")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _direct_houdini_enabled() -> bool:
|
|
47
|
-
return bool(settings.houdini_api_key.strip() and settings.houdini_api_secret.strip())
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _gateway_route_missing(status_code: int, payload: Any) -> bool:
|
|
51
|
-
if status_code == 404:
|
|
52
|
-
return True
|
|
53
|
-
if isinstance(payload, dict):
|
|
54
|
-
message = str(payload.get("error") or "").lower()
|
|
55
|
-
if "not found" in message:
|
|
56
|
-
return True
|
|
57
|
-
return False
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _normalize_error(payload: Any) -> str:
|
|
61
|
-
if isinstance(payload, dict):
|
|
62
|
-
message = payload.get("message") or payload.get("error") or payload.get("detail")
|
|
63
|
-
if message:
|
|
64
|
-
return str(message)
|
|
65
|
-
return "Houdini request failed."
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _response_text(response: Any) -> str:
|
|
69
|
-
try:
|
|
70
|
-
text = response.text
|
|
71
|
-
except Exception:
|
|
72
|
-
return ""
|
|
73
|
-
return str(text or "")
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _parse_json_response(response: Any, *, provider: str, operation: str) -> Any:
|
|
77
|
-
body = _response_text(response)
|
|
78
|
-
if not body.strip():
|
|
79
|
-
raise ProviderError(provider, f"{operation} returned an empty response body.")
|
|
80
|
-
try:
|
|
81
|
-
return response.json()
|
|
82
|
-
except ValueError as exc:
|
|
83
|
-
snippet = body.strip().replace("\n", " ")[:200]
|
|
84
|
-
detail = f": {snippet}" if snippet else ""
|
|
85
|
-
raise ProviderError(
|
|
86
|
-
provider,
|
|
87
|
-
f"{operation} returned invalid JSON{detail}",
|
|
88
|
-
) from exc
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _require_compliance_headers() -> dict[str, str]:
|
|
92
|
-
api_key = settings.houdini_api_key.strip()
|
|
93
|
-
api_secret = settings.houdini_api_secret.strip()
|
|
94
|
-
if not api_key or not api_secret:
|
|
95
|
-
raise ProviderError(
|
|
96
|
-
"houdini",
|
|
97
|
-
"Houdini API credentials are required. Set HOUDINI_API_KEY and HOUDINI_API_SECRET.",
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
user_ip = settings.houdini_user_ip.strip()
|
|
101
|
-
if not user_ip:
|
|
102
|
-
raise ProviderError(
|
|
103
|
-
"houdini",
|
|
104
|
-
"Houdini requires x-user-ip for compliance. Set HOUDINI_USER_IP first.",
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
user_agent = settings.houdini_user_agent.strip() or "AgentLayer/0.1.12"
|
|
108
|
-
user_timezone = settings.houdini_user_timezone.strip() or "UTC"
|
|
109
|
-
return {
|
|
110
|
-
"Accept": "application/json",
|
|
111
|
-
"Authorization": f"{api_key}:{api_secret}",
|
|
112
|
-
"x-user-ip": user_ip,
|
|
113
|
-
"x-user-agent": user_agent,
|
|
114
|
-
"x-user-timezone": user_timezone,
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _normalize_decimal_text(amount: Decimal) -> str:
|
|
119
|
-
text = format(amount, "f")
|
|
120
|
-
if "." in text:
|
|
121
|
-
text = text.rstrip("0").rstrip(".")
|
|
122
|
-
return text or "0"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _to_decimal(value: Any, *, field_name: str) -> Decimal:
|
|
126
|
-
try:
|
|
127
|
-
amount = Decimal(str(value))
|
|
128
|
-
except (InvalidOperation, TypeError, ValueError) as exc:
|
|
129
|
-
raise ProviderError("houdini", f"{field_name} must be a valid decimal amount.") from exc
|
|
130
|
-
if amount <= 0:
|
|
131
|
-
raise ProviderError("houdini", f"{field_name} must be greater than zero.")
|
|
132
|
-
return amount
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _normalize_token_record(token: dict[str, Any]) -> dict[str, Any]:
|
|
136
|
-
min_max = token.get("minMax") if isinstance(token.get("minMax"), dict) else {}
|
|
137
|
-
private_limits = min_max.get("private") if isinstance(min_max.get("private"), dict) else {}
|
|
138
|
-
return {
|
|
139
|
-
"id": str(token.get("id") or "").strip(),
|
|
140
|
-
"symbol": str(token.get("symbol") or "").strip(),
|
|
141
|
-
"name": str(token.get("name") or "").strip(),
|
|
142
|
-
"address": str(token.get("address") or "").strip(),
|
|
143
|
-
"chain": str(token.get("chain") or "").strip().lower(),
|
|
144
|
-
"decimals": token.get("decimals"),
|
|
145
|
-
"enabled": bool(token.get("enabled", True)),
|
|
146
|
-
"has_cex": bool(token.get("hasCex", False)),
|
|
147
|
-
"price": token.get("price"),
|
|
148
|
-
"min_max_private": private_limits,
|
|
149
|
-
"raw": token,
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _unwrap_gateway_payload(
|
|
154
|
-
status_code: int,
|
|
155
|
-
payload: Any,
|
|
156
|
-
*,
|
|
157
|
-
operation: str,
|
|
158
|
-
) -> Any:
|
|
159
|
-
if isinstance(payload, dict) and payload.get("ok") is False:
|
|
160
|
-
message = str(payload.get("error") or f"{operation} failed.")
|
|
161
|
-
raise ProviderError(
|
|
162
|
-
"houdini-gateway",
|
|
163
|
-
f"{operation} failed via provider gateway: {message}",
|
|
164
|
-
details=payload,
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
if status_code != 200:
|
|
168
|
-
message = payload
|
|
169
|
-
if isinstance(payload, dict):
|
|
170
|
-
message = payload.get("error") or payload
|
|
171
|
-
raise ProviderError(
|
|
172
|
-
"houdini-gateway",
|
|
173
|
-
f"{operation} failed via provider gateway: {message}",
|
|
174
|
-
details=payload if isinstance(payload, dict) else None,
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
return payload
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
async def _gateway_get(
|
|
181
|
-
path: str,
|
|
182
|
-
*,
|
|
183
|
-
params: dict[str, Any] | None,
|
|
184
|
-
operation: str,
|
|
185
|
-
) -> Any:
|
|
186
|
-
client = get_client()
|
|
187
|
-
response = await client.get(
|
|
188
|
-
f"{_gateway_base_url()}{path}",
|
|
189
|
-
params=params,
|
|
190
|
-
headers=_gateway_headers(),
|
|
191
|
-
)
|
|
192
|
-
payload = _parse_json_response(response, provider="houdini-gateway", operation=operation)
|
|
193
|
-
if _gateway_route_missing(response.status_code, payload) and _direct_houdini_enabled():
|
|
194
|
-
return None
|
|
195
|
-
return _unwrap_gateway_payload(
|
|
196
|
-
response.status_code,
|
|
197
|
-
payload,
|
|
198
|
-
operation=operation,
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
async def _gateway_post(
|
|
203
|
-
path: str,
|
|
204
|
-
*,
|
|
205
|
-
body: dict[str, Any],
|
|
206
|
-
operation: str,
|
|
207
|
-
) -> Any:
|
|
208
|
-
client = get_client()
|
|
209
|
-
response = await client.post(
|
|
210
|
-
f"{_gateway_base_url()}{path}",
|
|
211
|
-
json=body,
|
|
212
|
-
headers={**_gateway_headers(), "Content-Type": "application/json"},
|
|
213
|
-
)
|
|
214
|
-
payload = _parse_json_response(response, provider="houdini-gateway", operation=operation)
|
|
215
|
-
if _gateway_route_missing(response.status_code, payload) and _direct_houdini_enabled():
|
|
216
|
-
return None
|
|
217
|
-
return _unwrap_gateway_payload(
|
|
218
|
-
response.status_code,
|
|
219
|
-
payload,
|
|
220
|
-
operation=operation,
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
async def fetch_cex_tokens(*, chain: str) -> list[dict[str, Any]]:
|
|
225
|
-
normalized_chain = str(chain or "").strip().lower()
|
|
226
|
-
if not normalized_chain:
|
|
227
|
-
raise ProviderError("houdini", "chain is required when fetching Houdini tokens.")
|
|
228
|
-
|
|
229
|
-
cached = _CEX_TOKEN_CACHE.get(normalized_chain)
|
|
230
|
-
now = time.time()
|
|
231
|
-
if cached and now - cached[0] < _TOKEN_CACHE_TTL_SECONDS:
|
|
232
|
-
return cached[1]
|
|
233
|
-
|
|
234
|
-
tokens = await _fetch_cex_tokens_uncached(normalized_chain)
|
|
235
|
-
|
|
236
|
-
_CEX_TOKEN_CACHE[normalized_chain] = (now, tokens)
|
|
237
|
-
return tokens
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
async def _fetch_cex_tokens_uncached(chain: str) -> list[dict[str, Any]]:
|
|
241
|
-
page = 1
|
|
242
|
-
tokens: list[dict[str, Any]] = []
|
|
243
|
-
client = get_client()
|
|
244
|
-
while True:
|
|
245
|
-
gateway_payload = None
|
|
246
|
-
if _gateway_enabled():
|
|
247
|
-
gateway_payload = await _gateway_get(
|
|
248
|
-
"/v1/houdini/tokens",
|
|
249
|
-
params={
|
|
250
|
-
"hasCex": "true",
|
|
251
|
-
"chain": chain,
|
|
252
|
-
"pageSize": 100,
|
|
253
|
-
"page": page,
|
|
254
|
-
},
|
|
255
|
-
operation="Houdini tokens",
|
|
256
|
-
)
|
|
257
|
-
if gateway_payload is not None:
|
|
258
|
-
payload = gateway_payload
|
|
259
|
-
else:
|
|
260
|
-
response = await client.get(
|
|
261
|
-
f"{_base_url()}/tokens",
|
|
262
|
-
params={
|
|
263
|
-
"hasCex": "true",
|
|
264
|
-
"chain": chain,
|
|
265
|
-
"pageSize": 100,
|
|
266
|
-
"page": page,
|
|
267
|
-
},
|
|
268
|
-
headers=_require_compliance_headers(),
|
|
269
|
-
)
|
|
270
|
-
payload = _parse_json_response(response, provider="houdini", operation="Houdini tokens")
|
|
271
|
-
if response.status_code != 200:
|
|
272
|
-
raise ProviderError("houdini", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
273
|
-
page_tokens = payload.get("tokens")
|
|
274
|
-
if not isinstance(page_tokens, list):
|
|
275
|
-
raise ProviderError("houdini", "Unexpected tokens response from Houdini.")
|
|
276
|
-
tokens.extend(
|
|
277
|
-
_normalize_token_record(item)
|
|
278
|
-
for item in page_tokens
|
|
279
|
-
if isinstance(item, dict) and item.get("enabled", True)
|
|
280
|
-
)
|
|
281
|
-
total_pages = int(payload.get("totalPages") or 1)
|
|
282
|
-
if page >= total_pages:
|
|
283
|
-
break
|
|
284
|
-
page += 1
|
|
285
|
-
return tokens
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def _token_match_rank(term: str, token: dict[str, Any]) -> tuple[int, int, str]:
|
|
289
|
-
lowered = term.strip().lower()
|
|
290
|
-
symbol = str(token.get("symbol") or "").strip().lower()
|
|
291
|
-
name = str(token.get("name") or "").strip().lower()
|
|
292
|
-
address = str(token.get("address") or "").strip().lower()
|
|
293
|
-
token_id = str(token.get("id") or "").strip().lower()
|
|
294
|
-
|
|
295
|
-
if lowered == token_id:
|
|
296
|
-
return (0, 0, symbol)
|
|
297
|
-
if lowered == address:
|
|
298
|
-
return (1, 0, symbol)
|
|
299
|
-
if lowered == symbol:
|
|
300
|
-
return (2, 0, symbol)
|
|
301
|
-
if lowered == name:
|
|
302
|
-
return (3, 0, symbol)
|
|
303
|
-
if lowered in name or lowered in symbol or lowered in address:
|
|
304
|
-
return (4, len(name), symbol)
|
|
305
|
-
return (9, len(name), symbol)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
async def resolve_cex_token(*, term: str, chain: str) -> dict[str, Any]:
|
|
309
|
-
normalized_term = str(term or "").strip()
|
|
310
|
-
if not normalized_term:
|
|
311
|
-
raise ProviderError("houdini", "token term is required.")
|
|
312
|
-
|
|
313
|
-
tokens = await fetch_cex_tokens(chain=chain)
|
|
314
|
-
candidates = [
|
|
315
|
-
token
|
|
316
|
-
for token in tokens
|
|
317
|
-
if token.get("enabled", True) and token.get("has_cex", True)
|
|
318
|
-
]
|
|
319
|
-
ranked = sorted(
|
|
320
|
-
enumerate(candidates),
|
|
321
|
-
key=lambda pair: (_token_match_rank(normalized_term, pair[1]), pair[0]),
|
|
322
|
-
)
|
|
323
|
-
if not ranked or ranked[0][1] is None:
|
|
324
|
-
raise ProviderError(
|
|
325
|
-
"houdini",
|
|
326
|
-
f"Houdini does not expose a CEX token match for '{normalized_term}' on {chain}.",
|
|
327
|
-
)
|
|
328
|
-
best_rank = _token_match_rank(normalized_term, ranked[0][1])
|
|
329
|
-
if best_rank[0] >= 9:
|
|
330
|
-
raise ProviderError(
|
|
331
|
-
"houdini",
|
|
332
|
-
f"Houdini does not expose a CEX token match for '{normalized_term}' on {chain}.",
|
|
333
|
-
)
|
|
334
|
-
resolved = ranked[0][1]
|
|
335
|
-
if resolved.get("chain") != str(chain).strip().lower():
|
|
336
|
-
raise ProviderError(
|
|
337
|
-
"houdini",
|
|
338
|
-
f"Houdini token '{normalized_term}' resolved to the wrong chain ({resolved.get('chain')}).",
|
|
339
|
-
)
|
|
340
|
-
return resolved
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
async def fetch_private_quotes(
|
|
344
|
-
*,
|
|
345
|
-
from_token_id: str,
|
|
346
|
-
to_token_id: str,
|
|
347
|
-
amount_ui: Decimal,
|
|
348
|
-
) -> list[dict[str, Any]]:
|
|
349
|
-
payload = None
|
|
350
|
-
if _gateway_enabled():
|
|
351
|
-
payload = await _gateway_get(
|
|
352
|
-
"/v1/houdini/quotes/private",
|
|
353
|
-
params={
|
|
354
|
-
"from": from_token_id,
|
|
355
|
-
"to": to_token_id,
|
|
356
|
-
"amount": _normalize_decimal_text(amount_ui),
|
|
357
|
-
},
|
|
358
|
-
operation="Houdini private quotes",
|
|
359
|
-
)
|
|
360
|
-
if payload is None:
|
|
361
|
-
client = get_client()
|
|
362
|
-
response = await client.get(
|
|
363
|
-
f"{_base_url()}/quotes",
|
|
364
|
-
params={
|
|
365
|
-
"from": from_token_id,
|
|
366
|
-
"to": to_token_id,
|
|
367
|
-
"amount": _normalize_decimal_text(amount_ui),
|
|
368
|
-
"types": "private",
|
|
369
|
-
},
|
|
370
|
-
headers=_require_compliance_headers(),
|
|
371
|
-
)
|
|
372
|
-
payload = _parse_json_response(response, provider="houdini", operation="Houdini private quotes")
|
|
373
|
-
if response.status_code != 200:
|
|
374
|
-
raise ProviderError("houdini", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
375
|
-
quotes = payload.get("quotes")
|
|
376
|
-
if not isinstance(quotes, list):
|
|
377
|
-
raise ProviderError("houdini", "Unexpected quotes response from Houdini.")
|
|
378
|
-
private_quotes = [
|
|
379
|
-
item
|
|
380
|
-
for item in quotes
|
|
381
|
-
if isinstance(item, dict) and str(item.get("type") or "").strip().lower() == "private"
|
|
382
|
-
]
|
|
383
|
-
if not private_quotes:
|
|
384
|
-
raise ProviderError("houdini", "No private Houdini quote is available for this route.")
|
|
385
|
-
return private_quotes
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def select_best_private_quote(quotes: list[dict[str, Any]]) -> dict[str, Any]:
|
|
389
|
-
if not quotes:
|
|
390
|
-
raise ProviderError("houdini", "No private Houdini quote is available for this route.")
|
|
391
|
-
|
|
392
|
-
def _sort_key(item: dict[str, Any]) -> tuple[Decimal, Decimal]:
|
|
393
|
-
amount_out = _to_decimal(item.get("amountOut"), field_name="amountOut")
|
|
394
|
-
try:
|
|
395
|
-
duration = Decimal(str(item.get("duration") or 0))
|
|
396
|
-
except (InvalidOperation, TypeError, ValueError):
|
|
397
|
-
duration = Decimal("0")
|
|
398
|
-
return (amount_out, Decimal("-1") * duration)
|
|
399
|
-
|
|
400
|
-
return max(quotes, key=_sort_key)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
async def create_multi_swap(
|
|
404
|
-
*,
|
|
405
|
-
orders: list[dict[str, Any]],
|
|
406
|
-
) -> dict[str, Any]:
|
|
407
|
-
if not orders:
|
|
408
|
-
raise ProviderError("houdini", "orders are required.")
|
|
409
|
-
payload = None
|
|
410
|
-
if _gateway_enabled():
|
|
411
|
-
payload = await _gateway_post(
|
|
412
|
-
"/v1/houdini/exchanges/multi",
|
|
413
|
-
body={"orders": orders},
|
|
414
|
-
operation="Houdini multi create",
|
|
415
|
-
)
|
|
416
|
-
if payload is None:
|
|
417
|
-
client = get_client()
|
|
418
|
-
response = await client.post(
|
|
419
|
-
f"{_base_url()}/exchanges/multi",
|
|
420
|
-
json={"orders": orders},
|
|
421
|
-
headers={**_require_compliance_headers(), "Content-Type": "application/json"},
|
|
422
|
-
)
|
|
423
|
-
payload = _parse_json_response(response, provider="houdini", operation="Houdini multi create")
|
|
424
|
-
if response.status_code != 200:
|
|
425
|
-
raise ProviderError("houdini", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
426
|
-
if not isinstance(payload, dict) or not isinstance(payload.get("orders"), list):
|
|
427
|
-
raise ProviderError("houdini", "Unexpected multi-swap response from Houdini.")
|
|
428
|
-
return payload
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
async def create_exchange(
|
|
432
|
-
*,
|
|
433
|
-
quote_id: str,
|
|
434
|
-
destination_address: str,
|
|
435
|
-
) -> dict[str, Any]:
|
|
436
|
-
normalized_quote_id = str(quote_id).strip()
|
|
437
|
-
normalized_destination = str(destination_address).strip()
|
|
438
|
-
if not normalized_quote_id:
|
|
439
|
-
raise ProviderError("houdini", "quote_id is required.")
|
|
440
|
-
if not normalized_destination:
|
|
441
|
-
raise ProviderError("houdini", "destination_address is required.")
|
|
442
|
-
|
|
443
|
-
body = {
|
|
444
|
-
"quoteId": normalized_quote_id,
|
|
445
|
-
"addressTo": normalized_destination,
|
|
446
|
-
}
|
|
447
|
-
payload = None
|
|
448
|
-
if _gateway_enabled():
|
|
449
|
-
payload = await _gateway_post(
|
|
450
|
-
"/v1/houdini/exchanges",
|
|
451
|
-
body=body,
|
|
452
|
-
operation="Houdini exchange create",
|
|
453
|
-
)
|
|
454
|
-
if payload is None:
|
|
455
|
-
client = get_client()
|
|
456
|
-
response = await client.post(
|
|
457
|
-
f"{_base_url()}/exchanges",
|
|
458
|
-
json=body,
|
|
459
|
-
headers={**_require_compliance_headers(), "Content-Type": "application/json"},
|
|
460
|
-
)
|
|
461
|
-
payload = _parse_json_response(response, provider="houdini", operation="Houdini exchange create")
|
|
462
|
-
if response.status_code != 200:
|
|
463
|
-
raise ProviderError("houdini", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
464
|
-
if not isinstance(payload, dict) or not payload:
|
|
465
|
-
raise ProviderError("houdini", "Unexpected exchange response from Houdini.")
|
|
466
|
-
return payload
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
async def fetch_multi_status(*, multi_id: str) -> dict[str, Any]:
|
|
470
|
-
payload = None
|
|
471
|
-
normalized_multi_id = str(multi_id).strip()
|
|
472
|
-
if _gateway_enabled():
|
|
473
|
-
payload = await _gateway_get(
|
|
474
|
-
f"/v1/houdini/exchanges/multi/{normalized_multi_id}",
|
|
475
|
-
params=None,
|
|
476
|
-
operation="Houdini multi status",
|
|
477
|
-
)
|
|
478
|
-
if payload is None:
|
|
479
|
-
client = get_client()
|
|
480
|
-
response = await client.get(
|
|
481
|
-
f"{_base_url()}/exchanges/multi/{normalized_multi_id}",
|
|
482
|
-
headers=_require_compliance_headers(),
|
|
483
|
-
)
|
|
484
|
-
payload = _parse_json_response(response, provider="houdini", operation="Houdini multi status")
|
|
485
|
-
if response.status_code != 200:
|
|
486
|
-
raise ProviderError("houdini", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
487
|
-
if not isinstance(payload, dict) or not isinstance(payload.get("orders"), list):
|
|
488
|
-
raise ProviderError("houdini", "Unexpected multi-status response from Houdini.")
|
|
489
|
-
return payload
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
async def fetch_order_status(*, houdini_id: str) -> dict[str, Any]:
|
|
493
|
-
payload = None
|
|
494
|
-
normalized_houdini_id = str(houdini_id).strip()
|
|
495
|
-
if not normalized_houdini_id:
|
|
496
|
-
raise ProviderError("houdini", "houdini_id is required.")
|
|
497
|
-
if _gateway_enabled():
|
|
498
|
-
payload = await _gateway_get(
|
|
499
|
-
f"/v1/houdini/orders/{normalized_houdini_id}",
|
|
500
|
-
params=None,
|
|
501
|
-
operation="Houdini order status",
|
|
502
|
-
)
|
|
503
|
-
if payload is None:
|
|
504
|
-
client = get_client()
|
|
505
|
-
response = await client.get(
|
|
506
|
-
f"{_base_url()}/orders/{normalized_houdini_id}",
|
|
507
|
-
headers=_require_compliance_headers(),
|
|
508
|
-
)
|
|
509
|
-
payload = _parse_json_response(response, provider="houdini", operation="Houdini order status")
|
|
510
|
-
if response.status_code != 200:
|
|
511
|
-
raise ProviderError("houdini", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
512
|
-
if not isinstance(payload, dict) or not payload:
|
|
513
|
-
raise ProviderError("houdini", "Unexpected order-status response from Houdini.")
|
|
514
|
-
return payload
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
async def fetch_multi_solana_transactions(*, multi_id: str, sender: str) -> dict[str, Any]:
|
|
518
|
-
payload = None
|
|
519
|
-
normalized_multi_id = str(multi_id).strip()
|
|
520
|
-
normalized_sender = str(sender).strip()
|
|
521
|
-
if _gateway_enabled():
|
|
522
|
-
payload = await _gateway_get(
|
|
523
|
-
f"/v1/houdini/exchanges/multi/{normalized_multi_id}/tx",
|
|
524
|
-
params={"sender": normalized_sender},
|
|
525
|
-
operation="Houdini multi tx",
|
|
526
|
-
)
|
|
527
|
-
if payload is None:
|
|
528
|
-
client = get_client()
|
|
529
|
-
response = await client.get(
|
|
530
|
-
f"{_base_url()}/exchanges/multi/{normalized_multi_id}/tx",
|
|
531
|
-
params={"sender": normalized_sender},
|
|
532
|
-
headers=_require_compliance_headers(),
|
|
533
|
-
)
|
|
534
|
-
payload = _parse_json_response(response, provider="houdini", operation="Houdini multi tx")
|
|
535
|
-
if response.status_code != 200:
|
|
536
|
-
raise ProviderError("houdini", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
537
|
-
if not isinstance(payload, dict) or not isinstance(payload.get("transactions"), list):
|
|
538
|
-
raise ProviderError("houdini", "Unexpected Solana batch transaction response from Houdini.")
|
|
539
|
-
return payload
|