@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.
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
+ import time
6
7
  from dataclasses import asdict, dataclass, field
7
8
  from typing import Any
8
9
 
@@ -731,16 +732,74 @@ class AgentWalletBackend(ABC):
731
732
  input_mint: str,
732
733
  output_mint: str,
733
734
  amount_ui: float,
734
- slippage_bps: int = 50,
735
+ slippage_bps: int = 300,
735
736
  ) -> dict[str, Any]:
736
737
  raise WalletBackendError(f"{self.name} does not support swap previews.")
737
738
 
739
+ async def preview_swap_intent(
740
+ self,
741
+ input_mint: str,
742
+ output_mint: str,
743
+ amount_ui: float,
744
+ slippage_bps: int = 300,
745
+ minimum_output_amount_raw: int | None = None,
746
+ max_fee_lamports: int | None = None,
747
+ valid_for_seconds: int = 30,
748
+ max_attempts: int = 2,
749
+ ) -> dict[str, Any]:
750
+ preview = await self.preview_swap(
751
+ input_mint=input_mint,
752
+ output_mint=output_mint,
753
+ amount_ui=amount_ui,
754
+ slippage_bps=slippage_bps,
755
+ )
756
+ fee_summary = preview.get("fee_summary") if isinstance(preview.get("fee_summary"), dict) else {}
757
+ network_fee_lamports = fee_summary.get("network_fee_lamports")
758
+ if max_fee_lamports is None and isinstance(network_fee_lamports, int):
759
+ max_fee_lamports = max(network_fee_lamports * 3, network_fee_lamports + 100_000)
760
+ resolved_min_raw = minimum_output_amount_raw
761
+ if resolved_min_raw is None and isinstance(preview.get("minimum_output_amount_raw"), int):
762
+ resolved_min_raw = int(preview["minimum_output_amount_raw"])
763
+ output_decimals = preview.get("output_decimals")
764
+ minimum_output_amount_ui = preview.get("minimum_output_amount_ui")
765
+ if resolved_min_raw is not None and isinstance(output_decimals, int):
766
+ minimum_output_amount_ui = int(resolved_min_raw) / (10**output_decimals)
767
+ return {
768
+ "chain": preview.get("chain", "solana"),
769
+ "network": preview.get("network", getattr(self, "network", "unknown")),
770
+ "mode": "intent_preview",
771
+ "asset_type": "solana-swap-intent",
772
+ "owner": preview.get("owner"),
773
+ "input_mint": preview.get("input_mint", input_mint),
774
+ "output_mint": preview.get("output_mint", output_mint),
775
+ "input_amount_ui": preview.get("input_amount_ui", amount_ui),
776
+ "input_amount_raw": preview.get("input_amount_raw"),
777
+ "minimum_output_amount_raw": resolved_min_raw,
778
+ "minimum_output_amount_ui": minimum_output_amount_ui,
779
+ "indicative_output_amount_ui": preview.get("estimated_output_amount_ui"),
780
+ "indicative_output_amount_raw": preview.get("estimated_output_amount_raw"),
781
+ "max_slippage_bps": slippage_bps,
782
+ "slippage_bps": slippage_bps,
783
+ "max_fee_lamports": max_fee_lamports,
784
+ "valid_for_seconds": valid_for_seconds,
785
+ "valid_until_epoch_seconds": int(time.time()) + valid_for_seconds,
786
+ "max_attempts": max_attempts,
787
+ "allowed_providers": ["jupiter-ultra", "jupiter-metis"],
788
+ "recipient_policy": "owner-only",
789
+ "spend_policy": "exact-input",
790
+ "indicative_swap_provider": preview.get("swap_provider"),
791
+ "indicative_fee_summary": fee_summary,
792
+ "can_send": preview.get("can_send"),
793
+ "sign_only": preview.get("sign_only"),
794
+ "source": "swap-intent",
795
+ }
796
+
738
797
  async def prepare_swap(
739
798
  self,
740
799
  input_mint: str,
741
800
  output_mint: str,
742
801
  amount_ui: float,
743
- slippage_bps: int = 50,
802
+ slippage_bps: int = 300,
744
803
  ) -> dict[str, Any]:
745
804
  raise WalletBackendError(f"{self.name} does not support swap preparation.")
746
805
 
@@ -752,7 +811,7 @@ class AgentWalletBackend(ABC):
752
811
  input_mint=str(preview["input_mint"]),
753
812
  output_mint=str(preview["output_mint"]),
754
813
  amount_ui=float(preview["input_amount_ui"]),
755
- slippage_bps=int(preview.get("slippage_bps") or 50),
814
+ slippage_bps=int(preview.get("slippage_bps") or 300),
756
815
  )
757
816
 
758
817
  async def execute_swap(
@@ -760,7 +819,7 @@ class AgentWalletBackend(ABC):
760
819
  input_mint: str,
761
820
  output_mint: str,
762
821
  amount_ui: float,
763
- slippage_bps: int = 50,
822
+ slippage_bps: int = 300,
764
823
  ) -> dict[str, Any]:
765
824
  raise WalletBackendError(f"{self.name} does not support swaps.")
766
825
 
@@ -772,8 +831,55 @@ class AgentWalletBackend(ABC):
772
831
  input_mint=str(preview["input_mint"]),
773
832
  output_mint=str(preview["output_mint"]),
774
833
  amount_ui=float(preview["input_amount_ui"]),
775
- slippage_bps=int(preview.get("slippage_bps") or 50),
834
+ slippage_bps=int(preview.get("slippage_bps") or 300),
835
+ )
836
+
837
+ async def execute_swap_intent(
838
+ self,
839
+ *,
840
+ input_mint: str,
841
+ output_mint: str,
842
+ amount_ui: float,
843
+ slippage_bps: int = 300,
844
+ minimum_output_amount_raw: int | None = None,
845
+ max_fee_lamports: int | None = None,
846
+ valid_until_epoch_seconds: int | None = None,
847
+ max_attempts: int = 2,
848
+ ) -> dict[str, Any]:
849
+ if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
850
+ raise WalletBackendError("Approved swap intent has expired. Create a fresh intent preview.")
851
+ preview = await self.preview_swap(
852
+ input_mint=input_mint,
853
+ output_mint=output_mint,
854
+ amount_ui=amount_ui,
855
+ slippage_bps=slippage_bps,
776
856
  )
857
+ output_raw = preview.get("estimated_output_amount_raw")
858
+ if (
859
+ minimum_output_amount_raw is not None
860
+ and isinstance(output_raw, int)
861
+ and output_raw < int(minimum_output_amount_raw)
862
+ ):
863
+ raise WalletBackendError(
864
+ "Fresh swap quote is below the approved minimum output. Funds were not moved."
865
+ )
866
+ fee_summary = preview.get("fee_summary") if isinstance(preview.get("fee_summary"), dict) else {}
867
+ network_fee_lamports = fee_summary.get("network_fee_lamports")
868
+ if (
869
+ max_fee_lamports is not None
870
+ and isinstance(network_fee_lamports, int)
871
+ and network_fee_lamports > int(max_fee_lamports)
872
+ ):
873
+ raise WalletBackendError("Fresh swap fee exceeds the approved fee limit. Funds were not moved.")
874
+ result = await self.execute_swap_from_preview(preview)
875
+ result["intent_execution"] = {
876
+ "approved_minimum_output_amount_raw": minimum_output_amount_raw,
877
+ "approved_max_fee_lamports": max_fee_lamports,
878
+ "fresh_quote_used": True,
879
+ "attempt_count": 1,
880
+ "max_attempts": max_attempts,
881
+ }
882
+ return result
777
883
 
778
884
  async def get_bags_claimable_positions(
779
885
  self,
@@ -7,6 +7,7 @@ import base64
7
7
  import binascii
8
8
  import hashlib
9
9
  import json
10
+ import time
10
11
  from decimal import Decimal, InvalidOperation
11
12
  from typing import Any
12
13
 
@@ -48,6 +49,8 @@ TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
48
49
  NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112"
49
50
  STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111"
50
51
  HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS = 600
52
+ SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS = 300
53
+ SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS = 6_000_000
51
54
 
52
55
 
53
56
  def _load_signing_key():
@@ -2062,6 +2065,29 @@ class SolanaWalletBackend(AgentWalletBackend):
2062
2065
  parts.append(f"route fee {route_fee_bps} bps (already reflected in quoted output)")
2063
2066
  return "; ".join(parts)
2064
2067
 
2068
+ def _swap_fee_lamports(self, payload: dict[str, Any]) -> int | None:
2069
+ fee_summary = payload.get("fee_summary")
2070
+ if isinstance(fee_summary, dict):
2071
+ network_fee = _coerce_int(fee_summary.get("network_fee_lamports"))
2072
+ if network_fee is not None:
2073
+ return network_fee
2074
+ return None
2075
+
2076
+ def _default_swap_intent_max_fee_lamports(self, fee_summary: dict[str, Any]) -> int:
2077
+ estimated_fee = _coerce_int(fee_summary.get("network_fee_lamports")) or 0
2078
+ return max(
2079
+ estimated_fee * 3,
2080
+ estimated_fee + 100_000,
2081
+ SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS,
2082
+ )
2083
+
2084
+ def _swap_minimum_output_floor(self, *, out_amount_raw: int, slippage_bps: int) -> int:
2085
+ if out_amount_raw <= 0:
2086
+ return 0
2087
+ if slippage_bps <= 0:
2088
+ raise WalletBackendError("slippage_bps must be greater than zero.")
2089
+ return max(1, (out_amount_raw * max(0, 10_000 - slippage_bps)) // 10_000)
2090
+
2065
2091
  def _require_mainnet_bags(self, feature: str) -> None:
2066
2092
  if self.network != "mainnet":
2067
2093
  raise WalletBackendError(f"{feature} is only enabled for Solana mainnet.")
@@ -5426,7 +5452,8 @@ class SolanaWalletBackend(AgentWalletBackend):
5426
5452
  input_mint: str,
5427
5453
  output_mint: str,
5428
5454
  amount_ui: float,
5429
- slippage_bps: int = 50,
5455
+ slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
5456
+ exclude_routers: list[str] | None = None,
5430
5457
  ) -> dict[str, Any]:
5431
5458
  if self.network != "mainnet":
5432
5459
  raise WalletBackendError("Provider-routed swaps are only enabled for Solana mainnet.")
@@ -5447,29 +5474,38 @@ class SolanaWalletBackend(AgentWalletBackend):
5447
5474
  raise WalletBackendError("amount is too small for the input token decimals.")
5448
5475
 
5449
5476
  sender = await self.get_address()
5450
- quote_source = "jupiter-ultra"
5477
+ quote_source = "jupiter-v2-order"
5451
5478
  try:
5452
- quote = await jupiter.fetch_ultra_order(
5479
+ quote = await jupiter.fetch_swap_v2_order(
5453
5480
  input_mint=input_mint,
5454
5481
  output_mint=output_mint,
5455
5482
  amount_raw=raw_amount,
5456
5483
  taker=sender,
5457
- slippage_bps=slippage_bps,
5484
+ exclude_routers=exclude_routers,
5458
5485
  )
5459
5486
  except ProviderError:
5460
- quote = await jupiter.fetch_quote(
5461
- input_mint=input_mint,
5462
- output_mint=output_mint,
5463
- amount_raw=raw_amount,
5464
- slippage_bps=slippage_bps,
5465
- )
5466
- quote_source = "jupiter-metis"
5487
+ quote_source = "jupiter-ultra"
5488
+ try:
5489
+ quote = await jupiter.fetch_ultra_order(
5490
+ input_mint=input_mint,
5491
+ output_mint=output_mint,
5492
+ amount_raw=raw_amount,
5493
+ taker=sender,
5494
+ slippage_bps=slippage_bps,
5495
+ )
5496
+ except ProviderError:
5497
+ quote = await jupiter.fetch_quote(
5498
+ input_mint=input_mint,
5499
+ output_mint=output_mint,
5500
+ amount_raw=raw_amount,
5501
+ slippage_bps=slippage_bps,
5502
+ )
5503
+ quote_source = "jupiter-metis"
5467
5504
 
5468
5505
  out_amount_raw = int(quote.get("outAmount") or 0)
5469
- other_threshold_raw = int(
5470
- quote.get("otherAmountThreshold")
5471
- or quote.get("minOutAmount")
5472
- or 0
5506
+ other_threshold_raw = self._swap_minimum_output_floor(
5507
+ out_amount_raw=out_amount_raw,
5508
+ slippage_bps=slippage_bps,
5473
5509
  )
5474
5510
  fee_summary = self._build_swap_fee_summary(
5475
5511
  swap_provider=quote_source,
@@ -5505,12 +5541,115 @@ class SolanaWalletBackend(AgentWalletBackend):
5505
5541
  "source": quote_source,
5506
5542
  }
5507
5543
 
5544
+ async def preview_swap_intent(
5545
+ self,
5546
+ input_mint: str,
5547
+ output_mint: str,
5548
+ amount_ui: float,
5549
+ slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
5550
+ minimum_output_amount_raw: int | None = None,
5551
+ max_fee_lamports: int | None = None,
5552
+ valid_for_seconds: int = 120,
5553
+ max_attempts: int = 3,
5554
+ ) -> dict[str, Any]:
5555
+ if valid_for_seconds <= 0 or valid_for_seconds > 120:
5556
+ raise WalletBackendError("valid_for_seconds must be between 1 and 120.")
5557
+ if max_attempts <= 0 or max_attempts > 5:
5558
+ raise WalletBackendError("max_attempts must be between 1 and 5.")
5559
+ slippage_bps = max(int(slippage_bps), SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS)
5560
+ max_attempts = max(int(max_attempts), 3)
5561
+
5562
+ indicative = await self.preview_swap(
5563
+ input_mint=input_mint,
5564
+ output_mint=output_mint,
5565
+ amount_ui=amount_ui,
5566
+ slippage_bps=slippage_bps,
5567
+ )
5568
+ indicative_output_raw = int(indicative.get("estimated_output_amount_raw") or 0)
5569
+ slippage_floor_raw = self._swap_minimum_output_floor(
5570
+ out_amount_raw=indicative_output_raw,
5571
+ slippage_bps=slippage_bps,
5572
+ )
5573
+ requested_min_output_raw = (
5574
+ int(minimum_output_amount_raw)
5575
+ if minimum_output_amount_raw is not None
5576
+ else None
5577
+ )
5578
+ if requested_min_output_raw is not None:
5579
+ min_output_raw = min(requested_min_output_raw, slippage_floor_raw)
5580
+ minimum_output_policy = (
5581
+ "explicit_clamped_to_slippage_floor"
5582
+ if requested_min_output_raw > slippage_floor_raw
5583
+ else "explicit"
5584
+ )
5585
+ else:
5586
+ min_output_raw = slippage_floor_raw
5587
+ minimum_output_policy = "slippage_floor"
5588
+ if min_output_raw <= 0:
5589
+ raise WalletBackendError("minimum_output_amount_raw could not be derived from the indicative quote.")
5590
+ output_decimals = int(indicative.get("output_decimals") or 0)
5591
+ min_output_ui = min_output_raw / (10**output_decimals)
5592
+
5593
+ fee_summary = (
5594
+ indicative.get("fee_summary")
5595
+ if isinstance(indicative.get("fee_summary"), dict)
5596
+ else {}
5597
+ )
5598
+ fee_limit = (
5599
+ int(max_fee_lamports)
5600
+ if max_fee_lamports is not None
5601
+ else self._default_swap_intent_max_fee_lamports(fee_summary)
5602
+ )
5603
+ if fee_limit < 0:
5604
+ raise WalletBackendError("max_fee_lamports must be non-negative.")
5605
+
5606
+ return {
5607
+ "chain": "solana",
5608
+ "network": self.network,
5609
+ "mode": "intent_preview",
5610
+ "asset_type": "solana-swap-intent",
5611
+ "owner": indicative.get("owner"),
5612
+ "input_mint": indicative["input_mint"],
5613
+ "output_mint": indicative["output_mint"],
5614
+ "input_amount_ui": indicative["input_amount_ui"],
5615
+ "input_amount_raw": indicative["input_amount_raw"],
5616
+ "input_decimals": indicative.get("input_decimals"),
5617
+ "output_decimals": indicative.get("output_decimals"),
5618
+ "indicative_output_amount_ui": indicative.get("estimated_output_amount_ui"),
5619
+ "indicative_output_amount_raw": indicative.get("estimated_output_amount_raw"),
5620
+ "minimum_output_amount_ui": min_output_ui,
5621
+ "minimum_output_amount_raw": min_output_raw,
5622
+ "requested_minimum_output_amount_raw": requested_min_output_raw,
5623
+ "minimum_output_policy": minimum_output_policy,
5624
+ "max_slippage_bps": slippage_bps,
5625
+ "slippage_bps": slippage_bps,
5626
+ "max_fee_lamports": fee_limit,
5627
+ "max_fee_sol": fee_limit / solana_rpc.LAMPORTS_PER_SOL,
5628
+ "valid_for_seconds": valid_for_seconds,
5629
+ "valid_until_epoch_seconds": int(time.time()) + valid_for_seconds,
5630
+ "max_attempts": max_attempts,
5631
+ "allowed_providers": ["jupiter-v2-order", "jupiter-ultra", "jupiter-metis"],
5632
+ "recipient_policy": "owner-only",
5633
+ "spend_policy": "exact-input",
5634
+ "indicative_swap_provider": indicative.get("swap_provider"),
5635
+ "indicative_price_impact_pct": indicative.get("price_impact_pct"),
5636
+ "indicative_route_plan": indicative.get("route_plan", []),
5637
+ "indicative_fee_summary": fee_summary,
5638
+ "intent_note": (
5639
+ "This is an intent approval preview. Execute will fetch a fresh quote and "
5640
+ "only sign/send if it remains inside these approved limits."
5641
+ ),
5642
+ "can_send": self.get_capabilities().can_send_transaction,
5643
+ "sign_only": self.sign_only,
5644
+ "source": "swap-intent",
5645
+ }
5646
+
5508
5647
  async def execute_swap(
5509
5648
  self,
5510
5649
  input_mint: str,
5511
5650
  output_mint: str,
5512
5651
  amount_ui: float,
5513
- slippage_bps: int = 50,
5652
+ slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
5514
5653
  ) -> dict[str, Any]:
5515
5654
  preview = await self.preview_swap(
5516
5655
  input_mint=input_mint,
@@ -5520,17 +5659,23 @@ class SolanaWalletBackend(AgentWalletBackend):
5520
5659
  )
5521
5660
  return await self.execute_swap_from_preview(preview)
5522
5661
 
5523
- async def execute_swap_from_preview(
5662
+ async def _submit_prepared_swap(
5524
5663
  self,
5525
- preview: dict[str, Any],
5664
+ prepared: dict[str, Any],
5526
5665
  ) -> dict[str, Any]:
5527
- prepared = await self.prepare_swap_from_preview(preview)
5528
5666
  if self.sign_only:
5529
5667
  raise WalletBackendError(
5530
5668
  "This wallet backend is in sign-only mode. Disable sign_only to broadcast transactions."
5531
5669
  )
5532
5670
 
5533
- if prepared.get("swap_provider") == "jupiter-ultra":
5671
+ if prepared.get("swap_provider") == "jupiter-v2-order":
5672
+ submitted = await jupiter.execute_swap_v2_order(
5673
+ signed_transaction_base64=str(prepared["transaction_base64"]),
5674
+ request_id=str(prepared["request_id"]),
5675
+ last_valid_block_height=_coerce_int(prepared.get("last_valid_block_height")),
5676
+ )
5677
+ onchain_signature = submitted.get("signature") or submitted.get("txid")
5678
+ elif prepared.get("swap_provider") == "jupiter-ultra":
5534
5679
  submitted = await jupiter.execute_ultra_order(
5535
5680
  signed_transaction_base64=str(prepared["transaction_base64"]),
5536
5681
  request_id=str(prepared["request_id"]),
@@ -5583,12 +5728,132 @@ class SolanaWalletBackend(AgentWalletBackend):
5583
5728
  "source": prepared.get("swap_provider") or "jupiter-metis",
5584
5729
  }
5585
5730
 
5731
+ async def execute_swap_from_preview(
5732
+ self,
5733
+ preview: dict[str, Any],
5734
+ ) -> dict[str, Any]:
5735
+ prepared = await self.prepare_swap_from_preview(preview)
5736
+ return await self._submit_prepared_swap(prepared)
5737
+
5738
+ async def execute_swap_intent(
5739
+ self,
5740
+ *,
5741
+ input_mint: str,
5742
+ output_mint: str,
5743
+ amount_ui: float,
5744
+ slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
5745
+ minimum_output_amount_raw: int | None = None,
5746
+ max_fee_lamports: int | None = None,
5747
+ valid_until_epoch_seconds: int | None = None,
5748
+ max_attempts: int = 3,
5749
+ ) -> dict[str, Any]:
5750
+ if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
5751
+ raise WalletBackendError("Approved swap intent has expired. Create a fresh intent preview.")
5752
+ if max_attempts <= 0 or max_attempts > 5:
5753
+ raise WalletBackendError("max_attempts must be between 1 and 5.")
5754
+ slippage_bps = max(int(slippage_bps), SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS)
5755
+ max_attempts = max(int(max_attempts), 3)
5756
+
5757
+ attempts: list[dict[str, Any]] = []
5758
+ last_error: str | None = None
5759
+ for attempt_index in range(max_attempts):
5760
+ if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
5761
+ break
5762
+ try:
5763
+ exclude_routers = ["jupiterz"] if attempt_index > 0 else None
5764
+ preview = await self.preview_swap(
5765
+ input_mint=input_mint,
5766
+ output_mint=output_mint,
5767
+ amount_ui=amount_ui,
5768
+ slippage_bps=slippage_bps,
5769
+ exclude_routers=exclude_routers,
5770
+ )
5771
+ estimated_output_raw = int(preview.get("estimated_output_amount_raw") or 0)
5772
+ if (
5773
+ minimum_output_amount_raw is not None
5774
+ and estimated_output_raw < int(minimum_output_amount_raw)
5775
+ ):
5776
+ attempts.append(
5777
+ {
5778
+ "attempt": attempt_index + 1,
5779
+ "swap_provider": preview.get("swap_provider"),
5780
+ "rejected": "quote_below_minimum_output",
5781
+ "estimated_output_amount_raw": estimated_output_raw,
5782
+ "minimum_output_amount_raw": int(minimum_output_amount_raw),
5783
+ }
5784
+ )
5785
+ last_error = "Fresh swap quote is below the approved minimum output."
5786
+ continue
5787
+
5788
+ prepared = await self.prepare_swap_from_preview(preview)
5789
+ prepared_fee = self._swap_fee_lamports(prepared)
5790
+ if (
5791
+ max_fee_lamports is not None
5792
+ and prepared_fee is not None
5793
+ and prepared_fee > int(max_fee_lamports)
5794
+ ):
5795
+ attempts.append(
5796
+ {
5797
+ "attempt": attempt_index + 1,
5798
+ "swap_provider": prepared.get("swap_provider"),
5799
+ "rejected": "fee_above_limit",
5800
+ "fee_lamports": prepared_fee,
5801
+ "max_fee_lamports": int(max_fee_lamports),
5802
+ }
5803
+ )
5804
+ last_error = "Fresh swap fee exceeds the approved fee limit."
5805
+ continue
5806
+
5807
+ result = await self._submit_prepared_swap(prepared)
5808
+ result["intent_execution"] = {
5809
+ "approved_minimum_output_amount_raw": minimum_output_amount_raw,
5810
+ "approved_max_fee_lamports": max_fee_lamports,
5811
+ "fresh_quote_used": True,
5812
+ "attempt_count": attempt_index + 1,
5813
+ "max_attempts": max_attempts,
5814
+ "attempts": attempts
5815
+ + [
5816
+ {
5817
+ "attempt": attempt_index + 1,
5818
+ "swap_provider": prepared.get("swap_provider"),
5819
+ "status": "submitted",
5820
+ }
5821
+ ],
5822
+ }
5823
+ return result
5824
+ except (WalletBackendError, ProviderError) as exc:
5825
+ last_error = str(exc)
5826
+ attempts.append(
5827
+ {
5828
+ "attempt": attempt_index + 1,
5829
+ "rejected": "execution_error",
5830
+ "error": str(exc),
5831
+ }
5832
+ )
5833
+ if "sign-only mode" in str(exc).lower():
5834
+ break
5835
+ if attempt_index + 1 < max_attempts:
5836
+ await asyncio.sleep(min(0.5 * (attempt_index + 1), 1.5))
5837
+
5838
+ reason_suffix = f" Last reason: {last_error}" if last_error else ""
5839
+ raise WalletBackendError(
5840
+ "Solana swap intent execution failed within the approved limits. Funds were not moved."
5841
+ + reason_suffix,
5842
+ details={
5843
+ "reason": last_error,
5844
+ "attempts": attempts,
5845
+ "minimum_output_amount_raw": minimum_output_amount_raw,
5846
+ "max_fee_lamports": max_fee_lamports,
5847
+ "max_attempts": max_attempts,
5848
+ },
5849
+ )
5850
+
5586
5851
  async def prepare_swap(
5587
5852
  self,
5588
5853
  input_mint: str,
5589
5854
  output_mint: str,
5590
5855
  amount_ui: float,
5591
- slippage_bps: int = 50,
5856
+ slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
5592
5857
  ) -> dict[str, Any]:
5593
5858
  preview = await self.preview_swap(
5594
5859
  input_mint=input_mint,
@@ -5629,13 +5894,23 @@ class SolanaWalletBackend(AgentWalletBackend):
5629
5894
 
5630
5895
  swap_provider = str(preview.get("swap_provider") or "jupiter-metis")
5631
5896
  request_id = None
5632
- if swap_provider == "jupiter-ultra":
5897
+ if swap_provider in {"jupiter-v2-order", "jupiter-ultra"}:
5633
5898
  swap_build = preview["quote_response"]
5634
5899
  unsigned_transaction = VersionedTransaction.from_bytes(
5635
5900
  base64.b64decode(str(swap_build["transaction"]))
5636
5901
  )
5637
5902
  request_id = swap_build.get("requestId")
5638
- last_valid_block_height = swap_build.get("expireAt")
5903
+ blockhash_metadata = swap_build.get("blockhashWithMetadata")
5904
+ last_valid_block_height = (
5905
+ blockhash_metadata.get("lastValidBlockHeight")
5906
+ if isinstance(blockhash_metadata, dict)
5907
+ else None
5908
+ )
5909
+ if last_valid_block_height is None:
5910
+ last_valid_block_height = (
5911
+ swap_build.get("lastValidBlockHeight")
5912
+ or swap_build.get("expireAt")
5913
+ )
5639
5914
  prioritization_fee_lamports = swap_build.get("prioritizationFeeLamports")
5640
5915
  compute_unit_limit = swap_build.get("computeUnitLimit")
5641
5916
  else:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.24"
7
+ version = "0.1.26"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -16,7 +16,7 @@ from agent_wallet.file_ops import atomic_write_text, chmod_if_exists
16
16
  from agent_wallet.sealed_keys import resolve_sealed_keys_path, seal_keys, unseal_keys
17
17
  from security_utils import write_redacted_backup
18
18
 
19
- OPTIONAL_TOOLS = [
19
+ LEGACY_ALLOWLIST_TOOLS = [
20
20
  "get_wallet_capabilities",
21
21
  "get_wallet_address",
22
22
  "get_wallet_balance",
@@ -57,6 +57,62 @@ X402_TOOLS = [
57
57
  ]
58
58
 
59
59
 
60
+ def _extract_tool_allowlist_from_manifest(manifest_path: Path) -> list[str]:
61
+ try:
62
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
63
+ except (FileNotFoundError, json.JSONDecodeError):
64
+ return []
65
+
66
+ tools = manifest.get("contracts", {}).get("tools", [])
67
+ if not isinstance(tools, list):
68
+ return []
69
+
70
+ allowlist: list[str] = []
71
+ for item in tools:
72
+ tool_name = str(item).strip()
73
+ if tool_name and tool_name not in allowlist:
74
+ allowlist.append(tool_name)
75
+ return allowlist
76
+
77
+
78
+ def _load_extension_tool_allowlist(extension_path: Path) -> list[str]:
79
+ manifest_candidates = [
80
+ extension_path / "openclaw.plugin.json",
81
+ _repo_root() / ".openclaw" / "extensions" / "agent-wallet" / "openclaw.plugin.json",
82
+ ]
83
+ for manifest_path in manifest_candidates:
84
+ allowlist = _extract_tool_allowlist_from_manifest(manifest_path)
85
+ if allowlist:
86
+ return allowlist
87
+ return LEGACY_ALLOWLIST_TOOLS + X402_TOOLS
88
+
89
+
90
+ def _is_agent_wallet_extension_path(value: object) -> bool:
91
+ return "extensions/agent-wallet" in str(value).replace("\\", "/")
92
+
93
+
94
+ def _normalize_load_paths(paths: list[object], extension_path_text: str) -> list[str]:
95
+ normalized: list[str] = []
96
+ seen: set[str] = set()
97
+
98
+ for item in paths:
99
+ item_text = str(item).strip()
100
+ if not item_text:
101
+ continue
102
+ if "extensions/pay-bridge" in item_text:
103
+ continue
104
+ if _is_agent_wallet_extension_path(item_text) and item_text != extension_path_text:
105
+ continue
106
+ if item_text in seen:
107
+ continue
108
+ normalized.append(item_text)
109
+ seen.add(item_text)
110
+
111
+ if extension_path_text not in seen:
112
+ normalized.append(extension_path_text)
113
+ return normalized
114
+
115
+
60
116
  def _default_config_path() -> Path:
61
117
  return Path(os.path.expanduser("~/.openclaw/openclaw.json"))
62
118
 
@@ -239,9 +295,7 @@ def main() -> None:
239
295
  load = plugins.setdefault("load", {})
240
296
  paths = load.setdefault("paths", [])
241
297
  extension_path_text = str(Path(args.extension_path).expanduser().resolve())
242
- if extension_path_text not in paths:
243
- paths.append(extension_path_text)
244
- paths[:] = [item for item in paths if "extensions/pay-bridge" not in str(item)]
298
+ paths[:] = _normalize_load_paths(list(paths), extension_path_text)
245
299
 
246
300
  entries = plugins.setdefault("entries", {})
247
301
  effective_network = _normalize_network(args.backend, args.network)
@@ -306,7 +360,7 @@ def main() -> None:
306
360
  "pay_api_request",
307
361
  }
308
362
  also_allow[:] = [tool_name for tool_name in also_allow if tool_name not in removed_pay_tools]
309
- for tool_name in OPTIONAL_TOOLS + X402_TOOLS:
363
+ for tool_name in _load_extension_tool_allowlist(Path(extension_path_text)):
310
364
  if tool_name not in also_allow:
311
365
  also_allow.append(tool_name)
312
366