@agentlayer.tech/wallet 0.1.24 → 0.1.26

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.
@@ -222,6 +222,10 @@
222
222
  "type": "string",
223
223
  "description": "Optional Jupiter Swap API base URL."
224
224
  },
225
+ "jupiterSwapV2BaseUrl": {
226
+ "type": "string",
227
+ "description": "Optional Jupiter Swap API V2 base URL."
228
+ },
225
229
  "jupiterUltraBaseUrl": {
226
230
  "type": "string",
227
231
  "description": "Optional Jupiter Ultra API base URL."
@@ -11,9 +11,12 @@ Safety rules:
11
11
  - Use Kamino market/reserve reads before Kamino writes when the user needs lending context.
12
12
  - Use Aave account reads before Aave writes when the user needs EVM lending context.
13
13
  - For transfers, native staking, swaps, Aave writes, Jupiter Earn writes, and Kamino writes, use `preview` before `prepare` or `execute`.
14
+ - For Solana Jupiter swaps through `swap_solana_tokens`, prefer `intent_preview` then `intent_execute` after explicit chat confirmation. The user confirms risk limits; the backend refreshes the quote and only executes inside those limits.
15
+ - Solana swap intent defaults to at least 300 bps (3%) slippage, 120 seconds validity, and 3 fresh execution attempts. The backend computes the approved minimum output from the indicative output and slippage, clamps hand-tightened minimums to that floor, then executes through Jupiter Swap API V2 `/order` + `/execute` when available.
16
+ - Do not use legacy `execute` for Solana Jupiter swaps in OpenClaw. Exact quote-bound approval is too fragile for active markets and will be rejected by the bridge.
14
17
  - For `swap_solana_privately`, use `preview` and then `execute` after explicit user approval. Do not use `prepare` for this tool.
15
18
  - Use `prepare` only when the user clearly intends to produce an execution plan.
16
- - Use `execute` only after the host issues an `approval_token` bound to the exact previewed operation.
19
+ - Use `execute` only after the user explicitly confirms the shown summary in chat. OpenClaw handles the internal approval token; do not ask for `/approve`, buttons, popups, or a manual token. For Solana swap intents, the token is bound to the approved intent limits instead of one fragile quote.
17
20
  - On `mainnet`, require an approval token that includes explicit mainnet confirmation before any execution.
18
21
  - Before any `mainnet` execute, restate the network, operation type, asset, amount, and destination, validator, or stake account.
19
22
  - If a preview or prepare result includes `confirmation_summary` or `mainnet_warning`, surface that summary before asking for confirmation.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.26 - 2026-05-26
6
+
7
+ - Reworked Solana Jupiter swaps to prefer intent approvals, so OpenClaw confirms
8
+ risk limits and executes against a fresh quote instead of binding approval to
9
+ a fragile exact quote payload.
10
+ - Added Jupiter Swap API V2 `/order` + `/execute` routing with managed landing
11
+ support and fallback routing for Solana swaps.
12
+ - Hardened Solana swap intent defaults to 3% slippage, a 120-second execution
13
+ window, three fresh execution attempts, and safer minimum-output handling.
14
+ - Fixed Jupiter V2 execution payload compatibility by sending
15
+ `lastValidBlockHeight` in the string form expected by the API.
16
+ - Disabled legacy exact-preview Solana swap execute in the OpenClaw bridge to
17
+ prevent stale approval-token mismatches on active markets.
18
+
5
19
  ## v0.1.24 - 2026-05-23
6
20
 
7
21
  - Fixed the published npm package CLI metadata so
@@ -88,7 +88,7 @@ Current safe tools:
88
88
  - `transfer_sol`
89
89
  - `stake_sol_native`
90
90
  - `transfer_spl_token`
91
- - `swap_solana_tokens`
91
+ - `swap_solana_tokens` - Solana Jupiter swaps; prefer `intent_preview` -> chat confirmation -> `intent_execute` so execution refreshes the quote inside approved limits.
92
92
  - `swap_solana_privately` - Houdini-backed private Solana payout flow for same-token `SOL->SOL` or `USDC->USDC` transfers to a destination wallet.
93
93
  - `get_solana_private_swap_status`
94
94
  - `get_jupiter_earn_tokens`
@@ -129,8 +129,9 @@ Policy defaults:
129
129
  - read-only tools are always allowed
130
130
  - `prepare` requires `user_intent=true`
131
131
  - `prepare` does not return signed transaction bytes
132
- - `execute` requires a host-issued `approval_token` bound to the exact previewed operation
133
- - on mainnet networks, that `approval_token` must include explicit mainnet confirmation
132
+ - backend `execute` requires an internal authorization token bound to the exact previewed operation
133
+ - in OpenClaw, the extension handles that internal authorization automatically after the user explicitly confirms the shown summary in chat; do not ask OpenClaw users for `/approve`, buttons, popups, or a manual token
134
+ - on mainnet networks, that internal authorization must include explicit mainnet confirmation
134
135
  - on mainnet networks, preview and prepare responses include a `confirmation_summary` and `mainnet_warning` to force a clearer final confirmation step
135
136
 
136
137
  ## Install
@@ -45,6 +45,7 @@ class Settings(BaseSettings):
45
45
  wdk_evm_account_index: int = 0
46
46
 
47
47
  jupiter_api_base_url: str = "https://lite-api.jup.ag/swap/v1"
48
+ jupiter_swap_v2_api_base_url: str = "https://api.jup.ag/swap/v2"
48
49
  jupiter_ultra_api_base_url: str = "https://lite-api.jup.ag/ultra/v1"
49
50
  jupiter_price_api_base_url: str = "https://lite-api.jup.ag/price/v3"
50
51
  jupiter_portfolio_api_base_url: str = "https://api.jup.ag/portfolio/v1"
@@ -352,6 +352,28 @@ class OpenClawWalletAdapter:
352
352
  summary[key] = value
353
353
  return summary
354
354
 
355
+ if asset_type == "solana-swap-intent":
356
+ return {
357
+ "operation": action_label,
358
+ "network": str(payload.get("network") or getattr(self.backend, "network", "unknown")),
359
+ "owner": payload.get("owner"),
360
+ "input_mint": payload.get("input_mint"),
361
+ "output_mint": payload.get("output_mint"),
362
+ "input_amount_ui": payload.get("input_amount_ui"),
363
+ "input_amount_raw": payload.get("input_amount_raw"),
364
+ "minimum_output_amount_raw": payload.get("minimum_output_amount_raw"),
365
+ "minimum_output_amount_ui": payload.get("minimum_output_amount_ui"),
366
+ "max_slippage_bps": payload.get("max_slippage_bps"),
367
+ "slippage_bps": payload.get("slippage_bps"),
368
+ "max_fee_lamports": payload.get("max_fee_lamports"),
369
+ "valid_until_epoch_seconds": payload.get("valid_until_epoch_seconds"),
370
+ "valid_for_seconds": payload.get("valid_for_seconds"),
371
+ "max_attempts": payload.get("max_attempts"),
372
+ "allowed_providers": payload.get("allowed_providers"),
373
+ "recipient_policy": payload.get("recipient_policy"),
374
+ "spend_policy": payload.get("spend_policy"),
375
+ }
376
+
355
377
  if asset_type == "solana-lifi-cross-chain-swap":
356
378
  return {
357
379
  "operation": action_label,
@@ -2451,7 +2473,9 @@ class OpenClawWalletAdapter:
2451
2473
  name="swap_solana_tokens",
2452
2474
  description=(
2453
2475
  "Preview or execute a Solana token swap through Jupiter routing. "
2454
- "Use preview first, then execute only after explicit user approval."
2476
+ "Prefer intent_preview, then intent_execute after explicit chat confirmation; "
2477
+ "intent_execute fetches a fresh quote and only sends if it remains inside the approved limits. "
2478
+ "OpenClaw should not use legacy execute for Solana swaps because exact Jupiter quote payloads expire quickly."
2455
2479
  ),
2456
2480
  input_schema={
2457
2481
  "type": "object",
@@ -2470,12 +2494,28 @@ class OpenClawWalletAdapter:
2470
2494
  },
2471
2495
  "slippage_bps": {
2472
2496
  "type": "integer",
2473
- "description": "Optional slippage tolerance in basis points. Defaults to 50.",
2497
+ "description": "Optional slippage tolerance in basis points. Defaults to 300 (3%) for Solana swaps.",
2498
+ },
2499
+ "minimum_output_amount_raw": {
2500
+ "type": "integer",
2501
+ "description": "Optional approved minimum output in raw output token units for intent_preview. For intent swaps, overly strict values are clamped to the slippage floor.",
2502
+ },
2503
+ "max_fee_lamports": {
2504
+ "type": "integer",
2505
+ "description": "Optional maximum Solana network fee in lamports for intent_preview.",
2506
+ },
2507
+ "valid_for_seconds": {
2508
+ "type": "integer",
2509
+ "description": "Optional intent validity window in seconds. Intent swaps use at least 120 seconds, max 120.",
2510
+ },
2511
+ "max_attempts": {
2512
+ "type": "integer",
2513
+ "description": "Optional number of fresh quote/simulate/execute attempts. Intent swaps use at least 3 attempts, max 5.",
2474
2514
  },
2475
2515
  "mode": {
2476
2516
  "type": "string",
2477
- "enum": ["preview", "prepare", "execute"],
2478
- "description": "preview returns a quote, prepare returns an execution plan without signed transaction bytes, execute attempts to swap.",
2517
+ "enum": ["preview", "prepare", "execute", "intent_preview", "intent_execute"],
2518
+ "description": "intent_preview returns approved risk limits; intent_execute requotes and executes atomically inside those limits. Legacy preview/prepare/execute remains supported.",
2479
2519
  },
2480
2520
  "purpose": {
2481
2521
  "type": "string",
@@ -4428,19 +4468,6 @@ class OpenClawWalletAdapter:
4428
4468
  raise WalletBackendError(
4429
4469
  "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
4430
4470
  )
4431
- preview_summary = self._build_confirmation_summary(
4432
- action_label="Flash Trade open position",
4433
- payload=approved_preview,
4434
- )
4435
- summary_without_digest = {
4436
- key: value
4437
- for key, value in approval_summary_copy.items()
4438
- if key != "_preview_digest"
4439
- }
4440
- if preview_summary != summary_without_digest:
4441
- raise WalletBackendError(
4442
- "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
4443
- )
4444
4471
  execute_preview = dict(approved_preview)
4445
4472
  else:
4446
4473
  execute_preview = await active_backend.preview_flash_trade_open_position(
@@ -4567,19 +4594,6 @@ class OpenClawWalletAdapter:
4567
4594
  raise WalletBackendError(
4568
4595
  "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
4569
4596
  )
4570
- preview_summary = self._build_confirmation_summary(
4571
- action_label="Flash Trade close position",
4572
- payload=approved_preview,
4573
- )
4574
- summary_without_digest = {
4575
- key: value
4576
- for key, value in approval_summary_copy.items()
4577
- if key != "_preview_digest"
4578
- }
4579
- if preview_summary != summary_without_digest:
4580
- raise WalletBackendError(
4581
- "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
4582
- )
4583
4597
  execute_preview = dict(approved_preview)
4584
4598
  else:
4585
4599
  execute_preview = await active_backend.preview_flash_trade_close_position(
@@ -5274,7 +5288,11 @@ class OpenClawWalletAdapter:
5274
5288
  input_mint = args.get("input_mint")
5275
5289
  output_mint = args.get("output_mint")
5276
5290
  amount = args.get("amount")
5277
- slippage_bps = args.get("slippage_bps", 50)
5291
+ slippage_bps = args.get("slippage_bps", 300)
5292
+ minimum_output_amount_raw = args.get("minimum_output_amount_raw")
5293
+ max_fee_lamports = args.get("max_fee_lamports")
5294
+ valid_for_seconds = args.get("valid_for_seconds", 120)
5295
+ max_attempts = args.get("max_attempts", 3)
5278
5296
  mode = args.get("mode")
5279
5297
  purpose = args.get("purpose")
5280
5298
  user_intent = args.get("user_intent", False)
@@ -5288,10 +5306,28 @@ class OpenClawWalletAdapter:
5288
5306
  raise WalletBackendError("amount must be a positive number.")
5289
5307
  if not isinstance(slippage_bps, int) or slippage_bps <= 0:
5290
5308
  raise WalletBackendError("slippage_bps must be a positive integer.")
5291
- if mode not in {"preview", "prepare", "execute"}:
5292
- raise WalletBackendError("mode must be 'preview', 'prepare' or 'execute'.")
5309
+ if minimum_output_amount_raw is not None and (
5310
+ not isinstance(minimum_output_amount_raw, int) or minimum_output_amount_raw <= 0
5311
+ ):
5312
+ raise WalletBackendError("minimum_output_amount_raw must be a positive integer when provided.")
5313
+ if max_fee_lamports is not None and (
5314
+ not isinstance(max_fee_lamports, int) or max_fee_lamports < 0
5315
+ ):
5316
+ raise WalletBackendError("max_fee_lamports must be a non-negative integer when provided.")
5317
+ if not isinstance(valid_for_seconds, int) or valid_for_seconds <= 0 or valid_for_seconds > 120:
5318
+ raise WalletBackendError("valid_for_seconds must be an integer between 1 and 120.")
5319
+ if not isinstance(max_attempts, int) or max_attempts <= 0 or max_attempts > 5:
5320
+ raise WalletBackendError("max_attempts must be an integer between 1 and 5.")
5321
+ if mode not in {"preview", "prepare", "execute", "intent_preview", "intent_execute"}:
5322
+ raise WalletBackendError(
5323
+ "mode must be 'preview', 'prepare', 'execute', 'intent_preview' or 'intent_execute'."
5324
+ )
5293
5325
  if not isinstance(purpose, str) or not purpose.strip():
5294
5326
  raise WalletBackendError("purpose is required.")
5327
+ if mode in {"intent_preview", "intent_execute"}:
5328
+ slippage_bps = max(slippage_bps, 300)
5329
+ valid_for_seconds = max(valid_for_seconds, 120)
5330
+ max_attempts = max(max_attempts, 3)
5295
5331
 
5296
5332
  if mode == "preview":
5297
5333
  preview = await self.backend.preview_swap(
@@ -5310,6 +5346,27 @@ class OpenClawWalletAdapter:
5310
5346
  ),
5311
5347
  )
5312
5348
 
5349
+ if mode == "intent_preview":
5350
+ intent_preview = await self.backend.preview_swap_intent(
5351
+ input_mint=input_mint.strip(),
5352
+ output_mint=output_mint.strip(),
5353
+ amount_ui=float(amount),
5354
+ slippage_bps=slippage_bps,
5355
+ minimum_output_amount_raw=minimum_output_amount_raw,
5356
+ max_fee_lamports=max_fee_lamports,
5357
+ valid_for_seconds=valid_for_seconds,
5358
+ max_attempts=max_attempts,
5359
+ )
5360
+ return AgentToolResult(
5361
+ tool=tool_name,
5362
+ ok=True,
5363
+ data=self._annotate_sensitive_payload(
5364
+ intent_preview,
5365
+ action_label="Swap intent",
5366
+ mode="preview",
5367
+ ),
5368
+ )
5369
+
5313
5370
  if mode == "prepare":
5314
5371
  self._require_prepare_intent(user_intent)
5315
5372
  preview = await self.backend.preview_swap(
@@ -5331,6 +5388,103 @@ class OpenClawWalletAdapter:
5331
5388
  ),
5332
5389
  )
5333
5390
 
5391
+ if mode == "intent_execute":
5392
+ approval_payload = inspect_approval_token(
5393
+ approval_token,
5394
+ tool_name=tool_name,
5395
+ network=str(getattr(self.backend, "network", "unknown")),
5396
+ require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
5397
+ )
5398
+ approval_summary = approval_payload.get("binding", {}).get("summary")
5399
+ if not isinstance(approval_summary, dict):
5400
+ raise WalletBackendError(
5401
+ "approval_token does not match the requested operation. Generate a new intent preview and approval before execute."
5402
+ )
5403
+ expected_summary = {
5404
+ "operation": "Swap intent",
5405
+ "network": str(getattr(self.backend, "network", "unknown")),
5406
+ "input_mint": input_mint.strip(),
5407
+ "output_mint": output_mint.strip(),
5408
+ }
5409
+ for key, expected_value in expected_summary.items():
5410
+ if approval_summary.get(key) != expected_value:
5411
+ raise WalletBackendError(
5412
+ "approval_token does not match the requested swap intent. Generate a fresh intent preview and approval before execute."
5413
+ )
5414
+ current_owner = await self.backend.get_address()
5415
+ approved_owner = approval_summary.get("owner")
5416
+ if approved_owner and current_owner and str(approved_owner) != str(current_owner):
5417
+ raise WalletBackendError(
5418
+ "approval_token does not match the active wallet owner. Generate a fresh intent preview and approval before execute."
5419
+ )
5420
+ try:
5421
+ approved_amount = float(approval_summary.get("input_amount_ui"))
5422
+ approved_slippage = int(
5423
+ approval_summary.get("max_slippage_bps")
5424
+ if approval_summary.get("max_slippage_bps") is not None
5425
+ else approval_summary.get("slippage_bps")
5426
+ )
5427
+ except (TypeError, ValueError):
5428
+ raise WalletBackendError(
5429
+ "approval_token does not match the requested swap intent. Generate a fresh intent preview and approval before execute."
5430
+ )
5431
+ if approved_amount != float(amount) or approved_slippage != slippage_bps:
5432
+ raise WalletBackendError(
5433
+ "approval_token does not match the requested swap intent. Generate a fresh intent preview and approval before execute."
5434
+ )
5435
+ if approval_summary.get("recipient_policy") != "owner-only":
5436
+ raise WalletBackendError("approved swap intent recipient policy is invalid.")
5437
+ if approval_summary.get("spend_policy") != "exact-input":
5438
+ raise WalletBackendError("approved swap intent spend policy is invalid.")
5439
+
5440
+ approval_summary_copy = dict(approval_summary)
5441
+ self._require_execute_approval(
5442
+ approval_token=approval_token,
5443
+ tool_name=tool_name,
5444
+ summary=approval_summary_copy,
5445
+ action_label="Swap intent",
5446
+ )
5447
+ try:
5448
+ approved_min_output_raw = (
5449
+ int(approval_summary_copy["minimum_output_amount_raw"])
5450
+ if approval_summary_copy.get("minimum_output_amount_raw") is not None
5451
+ else None
5452
+ )
5453
+ approved_max_fee_lamports = (
5454
+ int(approval_summary_copy["max_fee_lamports"])
5455
+ if approval_summary_copy.get("max_fee_lamports") is not None
5456
+ else None
5457
+ )
5458
+ approved_valid_until = (
5459
+ int(approval_summary_copy["valid_until_epoch_seconds"])
5460
+ if approval_summary_copy.get("valid_until_epoch_seconds") is not None
5461
+ else None
5462
+ )
5463
+ approved_max_attempts = int(approval_summary_copy.get("max_attempts") or max_attempts)
5464
+ except (TypeError, ValueError):
5465
+ raise WalletBackendError(
5466
+ "approval_token does not contain valid swap intent limits. Generate a fresh intent preview and approval before execute."
5467
+ )
5468
+ result = await self.backend.execute_swap_intent(
5469
+ input_mint=input_mint.strip(),
5470
+ output_mint=output_mint.strip(),
5471
+ amount_ui=float(amount),
5472
+ slippage_bps=slippage_bps,
5473
+ minimum_output_amount_raw=approved_min_output_raw,
5474
+ max_fee_lamports=approved_max_fee_lamports,
5475
+ valid_until_epoch_seconds=approved_valid_until,
5476
+ max_attempts=approved_max_attempts,
5477
+ )
5478
+ return AgentToolResult(
5479
+ tool=tool_name,
5480
+ ok=True,
5481
+ data=self._annotate_sensitive_payload(
5482
+ result,
5483
+ action_label="Swap",
5484
+ mode="execute",
5485
+ ),
5486
+ )
5487
+
5334
5488
  approval_payload = inspect_approval_token(
5335
5489
  approval_token,
5336
5490
  tool_name=tool_name,
@@ -5376,19 +5530,6 @@ class OpenClawWalletAdapter:
5376
5530
  raise WalletBackendError(
5377
5531
  "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
5378
5532
  )
5379
- preview_summary = self._build_confirmation_summary(
5380
- action_label="Swap",
5381
- payload=approved_preview,
5382
- )
5383
- summary_without_digest = {
5384
- key: value
5385
- for key, value in approval_summary_copy.items()
5386
- if key != "_preview_digest"
5387
- }
5388
- if preview_summary != summary_without_digest:
5389
- raise WalletBackendError(
5390
- "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
5391
- )
5392
5533
  execute_preview = dict(approved_preview)
5393
5534
  else:
5394
5535
  execute_preview = await self.backend.preview_swap(
@@ -5525,19 +5666,6 @@ class OpenClawWalletAdapter:
5525
5666
  raise WalletBackendError(
5526
5667
  "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
5527
5668
  )
5528
- preview_summary = self._build_confirmation_summary(
5529
- action_label="Solana private swap",
5530
- payload=approved_preview,
5531
- )
5532
- summary_without_digest = {
5533
- key: value
5534
- for key, value in approval_summary_copy.items()
5535
- if key != "_preview_digest"
5536
- }
5537
- if preview_summary != summary_without_digest:
5538
- raise WalletBackendError(
5539
- "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
5540
- )
5541
5669
  execute_preview = dict(approved_preview)
5542
5670
 
5543
5671
  self._require_execute_approval(
@@ -5593,20 +5721,6 @@ class OpenClawWalletAdapter:
5593
5721
  raise WalletBackendError(
5594
5722
  "approved preview payload does not match the approval token. Generate a new preview and approval before continue."
5595
5723
  )
5596
- preview_summary = self._build_confirmation_summary(
5597
- action_label="Solana private swap",
5598
- payload=approved_preview,
5599
- )
5600
- summary_without_digest = {
5601
- key: value
5602
- for key, value in approval_summary_copy.items()
5603
- if key != "_preview_digest"
5604
- }
5605
- if preview_summary != summary_without_digest:
5606
- raise WalletBackendError(
5607
- "approved preview payload does not match the approval token. Generate a new preview and approval before continue."
5608
- )
5609
-
5610
5724
  self._require_execute_approval(
5611
5725
  approval_token=approval_token,
5612
5726
  tool_name="swap_solana_privately",
@@ -93,6 +93,7 @@ def _apply_config_overrides(config: dict[str, Any]) -> None:
93
93
  ),
94
94
  "openclawHome": ("OPENCLAW_HOME", config.get("openclawHome"), True),
95
95
  "jupiterBaseUrl": ("JUPITER_API_BASE_URL", config.get("jupiterBaseUrl"), True),
96
+ "jupiterSwapV2BaseUrl": ("JUPITER_SWAP_V2_API_BASE_URL", config.get("jupiterSwapV2BaseUrl"), True),
96
97
  "jupiterUltraBaseUrl": ("JUPITER_ULTRA_API_BASE_URL", config.get("jupiterUltraBaseUrl"), True),
97
98
  "jupiterPriceBaseUrl": ("JUPITER_PRICE_API_BASE_URL", config.get("jupiterPriceBaseUrl"), True),
98
99
  "jupiterPortfolioBaseUrl": (
@@ -91,6 +91,13 @@ def _direct_jupiter_enabled() -> bool:
91
91
  return bool(settings.jupiter_api_key.strip())
92
92
 
93
93
 
94
+ def _swap_v2_base_url() -> str:
95
+ return os.getenv(
96
+ "JUPITER_SWAP_V2_API_BASE_URL",
97
+ settings.jupiter_swap_v2_api_base_url,
98
+ ).strip().rstrip("/")
99
+
100
+
94
101
  def _unwrap_gateway_payload(
95
102
  status_code: int,
96
103
  payload: Any,
@@ -367,6 +374,52 @@ async def fetch_ultra_order(
367
374
  return data
368
375
 
369
376
 
377
+ async def fetch_swap_v2_order(
378
+ *,
379
+ input_mint: str,
380
+ output_mint: str,
381
+ amount_raw: int,
382
+ taker: str,
383
+ slippage_bps: int | str | None = None,
384
+ exclude_routers: list[str] | None = None,
385
+ swap_mode: str = "ExactIn",
386
+ ) -> dict[str, Any]:
387
+ """Fetch a Jupiter Swap API V2 meta-aggregator order."""
388
+ client = get_client()
389
+ params: dict[str, Any] = {
390
+ "inputMint": input_mint,
391
+ "outputMint": output_mint,
392
+ "amount": str(amount_raw),
393
+ "taker": taker,
394
+ }
395
+ if swap_mode != "ExactIn":
396
+ params["swapMode"] = swap_mode
397
+ if slippage_bps is not None:
398
+ params["slippageBps"] = str(slippage_bps)
399
+ if exclude_routers:
400
+ params["excludeRouters"] = ",".join(str(item).strip() for item in exclude_routers if str(item).strip())
401
+
402
+ response = await client.get(
403
+ f"{_swap_v2_base_url()}/order",
404
+ params=params,
405
+ headers=_headers(),
406
+ )
407
+ if response.status_code != 200:
408
+ raise ProviderError("jupiter-v2", f"HTTP {response.status_code}: {response.text[:300]}")
409
+ data = response.json()
410
+ if not isinstance(data, dict):
411
+ raise ProviderError("jupiter-v2", "Unexpected order response from Jupiter Swap V2.")
412
+ if data.get("error") or data.get("errorCode"):
413
+ raise ProviderError(
414
+ "jupiter-v2",
415
+ str(data.get("error") or data.get("errorCode") or "Unknown Swap V2 order error."),
416
+ details=data,
417
+ )
418
+ if "outAmount" not in data:
419
+ raise ProviderError("jupiter-v2", "Unexpected order response from Jupiter Swap V2.")
420
+ return data
421
+
422
+
370
423
  async def build_swap_transaction(
371
424
  *,
372
425
  user_public_key: str,
@@ -477,6 +530,42 @@ async def execute_ultra_order(
477
530
  return data
478
531
 
479
532
 
533
+ async def execute_swap_v2_order(
534
+ *,
535
+ signed_transaction_base64: str,
536
+ request_id: str,
537
+ last_valid_block_height: int | str | None = None,
538
+ ) -> dict[str, Any]:
539
+ """Execute a signed Jupiter Swap API V2 order."""
540
+ client = get_client()
541
+ body: dict[str, Any] = {
542
+ "signedTransaction": signed_transaction_base64,
543
+ "requestId": request_id,
544
+ }
545
+ if last_valid_block_height is not None:
546
+ body["lastValidBlockHeight"] = str(last_valid_block_height)
547
+ response = await client.post(
548
+ f"{_swap_v2_base_url()}/execute",
549
+ json=body,
550
+ headers={**_headers(), "Content-Type": "application/json"},
551
+ )
552
+ if response.status_code != 200:
553
+ raise ProviderError("jupiter-v2", f"HTTP {response.status_code}: {response.text[:300]}")
554
+ data = response.json()
555
+ if not isinstance(data, dict):
556
+ raise ProviderError("jupiter-v2", "Unexpected execute response from Jupiter Swap V2.")
557
+ if data.get("error") or data.get("errorCode"):
558
+ raise ProviderError(
559
+ "jupiter-v2",
560
+ str(data.get("error") or data.get("errorCode") or "Unknown Swap V2 execute error."),
561
+ details=data,
562
+ )
563
+ if str(data.get("status") or "").strip().lower() == "failed":
564
+ message = data.get("error") or data.get("code") or "Swap V2 execute failed."
565
+ raise ProviderError("jupiter-v2", str(message), details=data)
566
+ return data
567
+
568
+
480
569
  async def fetch_prices(
481
570
  *,
482
571
  mints: list[str],