@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.
- package/.openclaw/extensions/agent-wallet/README.md +3 -3
- package/.openclaw/extensions/agent-wallet/dist/index.js +138 -96
- package/.openclaw/extensions/agent-wallet/index.ts +138 -96
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +4 -0
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +4 -1
- package/CHANGELOG.md +14 -0
- package/agent-wallet/README.md +4 -3
- package/agent-wallet/agent_wallet/config.py +1 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +187 -73
- package/agent-wallet/agent_wallet/openclaw_cli.py +1 -0
- package/agent-wallet/agent_wallet/providers/jupiter.py +89 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +111 -5
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +298 -23
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_openclaw_local_config.py +59 -5
- package/agent-wallet/skills/wallet-operator/SKILL.md +10 -6
- package/package.json +1 -1
- package/wdk-evm-wallet/package-lock.json +123 -309
- package/wdk-evm-wallet/package.json +10 -3
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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-
|
|
5477
|
+
quote_source = "jupiter-v2-order"
|
|
5451
5478
|
try:
|
|
5452
|
-
quote = await jupiter.
|
|
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
|
-
|
|
5484
|
+
exclude_routers=exclude_routers,
|
|
5458
5485
|
)
|
|
5459
5486
|
except ProviderError:
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
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 =
|
|
5470
|
-
|
|
5471
|
-
|
|
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 =
|
|
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
|
|
5662
|
+
async def _submit_prepared_swap(
|
|
5524
5663
|
self,
|
|
5525
|
-
|
|
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-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|