@agentlayer.tech/wallet 0.1.36 → 0.1.43

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.
@@ -2,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
5
- "version": "0.1.36",
5
+ "version": "0.1.43",
6
6
  "contracts": {
7
7
  "tools": [
8
8
  "close_empty_token_accounts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayertech/agent-wallet-plugin",
3
- "version": "0.1.36",
3
+ "version": "0.1.43",
4
4
  "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN ../../../LICENSE",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.41 - 2026-06-07
6
+
7
+ - Hardened Uniswap Trading API swap execution on active EVM mainnet markets.
8
+ - Stabilized the Uniswap quote fingerprint so `preview -> execute` binds to
9
+ the swap intent rather than a per-block quoted output amount, preventing
10
+ harmless repricing from failing execute with `swap_quote_changed`.
11
+ - Raised the default Uniswap slippage floor from `50` bps to `300` bps
12
+ (`3%`) so normal drift during the approval/execution window on Base no
13
+ longer causes avoidable failures. Per-call and env overrides still apply.
14
+ - Made the local `wdk-evm-wallet` daemon version-aware and self-refreshing.
15
+ - `/health` now reports the real launcher version instead of a hardcoded
16
+ `0.1.0`.
17
+ - Local EVM autostart compares the running daemon version against the on-disk
18
+ launcher version and automatically restarts a stale daemon after local
19
+ release/install, avoiding old code staying resident in memory.
20
+ - Extended the long-running EVM HTTP timeout policy to the Uniswap
21
+ `/v1/evm/uniswap/swap/{quote,send}` routes so approve+swap flows on Base do
22
+ not time out client-side while the daemon is still finishing the on-chain
23
+ operation.
24
+
25
+ ## v0.1.37 - 2026-06-05
26
+
27
+ - Fixed the Claude Code / Codex MCP bridge failing to start with
28
+ `MCP error -32000: Connection closed` when the plugin is run through a
29
+ marketplace symlink. run_mcp.sh resolved PLUGIN_ROOT with a logical `pwd`, so
30
+ the symlink stayed in the path and the sibling-codex fallback (`../../../codex`)
31
+ collapsed lexically into a non-existent path. The launcher now resolves paths
32
+ physically (`pwd -P`), keeping the `..` arithmetic consistent with the real
33
+ layout, with a regression test that drives the launcher through a symlink.
34
+
35
+ ## v0.1.36 - 2026-06-03
36
+
37
+ - Installer no longer pins a redundant `OPENCLAW_HOME` into the version-controlled
38
+ bundle `.mcp.json` when the install home is the default `~/.openclaw`
39
+ (run_mcp.sh already derives it). Any stale pin is removed so the tracked file
40
+ self-heals to a clean state; non-default homes still pin as before.
41
+
5
42
  ## v0.1.35 - 2026-06-03
6
43
 
7
44
  - Hardened the Codex / Claude Code MCP wallet bridge:
package/README.md CHANGED
@@ -6,10 +6,18 @@
6
6
  [![docs](https://img.shields.io/badge/docs-agent--layer.tech-blue)](https://docs.agent-layer.tech/)
7
7
  [![license](https://img.shields.io/github/license/lopushok9/Agent-Layer)](https://github.com/lopushok9/Agent-Layer/blob/main/LICENSE)
8
8
 
9
+ For Openclaw:
10
+
9
11
  ```bash
10
12
  npx @agentlayer.tech/wallet install --yes
11
13
  ```
12
14
 
15
+ For Codex:
16
+
17
+ ```bash
18
+ npx @agentlayer.tech/wallet install --yes && npx @agentlayer.tech/wallet codex install --yes
19
+ ```
20
+
13
21
  For Hermes:
14
22
 
15
23
  ```bash
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.36
1
+ 0.1.43
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Keep in sync with package.json, pyproject.toml, and the npm installer version.
4
4
  # scripts/check_release_version.mjs enforces this on release.
5
- __version__ = "0.1.36"
5
+ __version__ = "0.1.43"
6
6
 
7
7
  __all__ = [
8
8
  "config",
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import secrets
8
+ import shutil
9
+ import signal
8
10
  import subprocess
9
11
  import time
10
12
  from pathlib import Path
@@ -50,12 +52,103 @@ def _health_url(service_url: str) -> str:
50
52
  return f"{service_url.rstrip('/')}/health"
51
53
 
52
54
 
53
- def _service_is_healthy(service_url: str) -> bool:
55
+ def _service_health(service_url: str) -> dict[str, Any] | None:
56
+ """Return the parsed /health payload, or None if the service is down.
57
+
58
+ An empty dict means the service answered 200 but the body was unparseable —
59
+ treated as "running, version unknown" so we never restart on a parse blip.
60
+ """
54
61
  try:
55
62
  with urlopen(_health_url(service_url), timeout=1.5) as response:
56
- return int(getattr(response, "status", 0) or 0) == 200
63
+ if int(getattr(response, "status", 0) or 0) != 200:
64
+ return None
65
+ raw = response.read()
57
66
  except (URLError, TimeoutError, OSError):
58
- return False
67
+ return None
68
+ try:
69
+ payload = json.loads(raw.decode("utf-8"))
70
+ except (ValueError, UnicodeDecodeError):
71
+ return {}
72
+ return payload if isinstance(payload, dict) else {}
73
+
74
+
75
+ def _service_is_healthy(service_url: str) -> bool:
76
+ return _service_health(service_url) is not None
77
+
78
+
79
+ def _read_on_disk_service_version(wallet_root: Path) -> str | None:
80
+ """Version the launcher in wallet_root would report once (re)started."""
81
+ try:
82
+ pkg = json.loads((wallet_root / "package.json").read_text(encoding="utf-8"))
83
+ except (OSError, ValueError):
84
+ return None
85
+ version = str(pkg.get("version") or "").strip()
86
+ return version or None
87
+
88
+
89
+ def _listening_pids(port: int) -> list[int]:
90
+ """PIDs LISTENing on a local TCP port (via lsof), excluding our own."""
91
+ lsof = shutil.which("lsof")
92
+ if not lsof:
93
+ return []
94
+ try:
95
+ completed = subprocess.run( # noqa: S603
96
+ [lsof, "-t", "-i", f"tcp:{port}", "-sTCP:LISTEN"],
97
+ capture_output=True,
98
+ text=True,
99
+ timeout=5,
100
+ )
101
+ except (OSError, subprocess.SubprocessError):
102
+ return []
103
+ pids: list[int] = []
104
+ for token in completed.stdout.split():
105
+ token = token.strip()
106
+ if token.isdigit():
107
+ pid = int(token)
108
+ if pid != os.getpid():
109
+ pids.append(pid)
110
+ return pids
111
+
112
+
113
+ def _stop_local_service(service_url: str) -> None:
114
+ """Gracefully stop a local wdk-evm-wallet daemon so a fresh one can start.
115
+
116
+ SIGTERM the listener(s), wait for /health to drop, then SIGKILL as a fallback.
117
+ """
118
+ port = urlparse(service_url).port or 8081
119
+ pids = _listening_pids(port)
120
+ if not pids:
121
+ # Nothing identifiable to stop (e.g. lsof missing). Surface a clear,
122
+ # actionable error rather than racing into an EADDRINUSE start failure.
123
+ raise WalletBackendError(
124
+ f"A stale wdk-evm-wallet is running on port {port} but could not be "
125
+ "identified to restart it. Stop it manually and retry."
126
+ )
127
+ for pid in pids:
128
+ try:
129
+ os.kill(pid, signal.SIGTERM)
130
+ except ProcessLookupError:
131
+ continue
132
+ except PermissionError as exc:
133
+ raise WalletBackendError(
134
+ f"Cannot stop stale wdk-evm-wallet (pid {pid}): {exc}."
135
+ ) from exc
136
+ deadline = time.time() + 10.0
137
+ while time.time() < deadline:
138
+ if _service_health(service_url) is None:
139
+ return
140
+ time.sleep(0.3)
141
+ for pid in _listening_pids(port):
142
+ try:
143
+ os.kill(pid, signal.SIGKILL)
144
+ except ProcessLookupError:
145
+ continue
146
+ deadline = time.time() + 5.0
147
+ while time.time() < deadline:
148
+ if _service_health(service_url) is None:
149
+ return
150
+ time.sleep(0.3)
151
+ raise WalletBackendError(f"Failed to stop stale wdk-evm-wallet on port {port}.")
59
152
 
60
153
 
61
154
  def _is_local_service_url(service_url: str) -> bool:
@@ -80,13 +173,28 @@ def _resolve_local_wdk_evm_root() -> Path | None:
80
173
 
81
174
 
82
175
  def _auto_start_local_service(service_url: str, network: str) -> None:
83
- if _service_is_healthy(service_url):
84
- return
176
+ wallet_root = _resolve_local_wdk_evm_root()
177
+ health = _service_health(service_url)
178
+ if health is not None:
179
+ # Already running. The daemon loads code once at boot (no hot-reload), so a
180
+ # long-running process keeps serving stale code after a release. Restart it
181
+ # only when its reported version differs from the on-disk launcher version —
182
+ # comparing against the version the restarted daemon will itself report keeps
183
+ # this idempotent (steady state = no-op) and free of restart loops. Remote
184
+ # (non-local) healthy services we don't manage are left untouched.
185
+ if not _is_local_service_url(service_url):
186
+ return
187
+ expected_version = (
188
+ _read_on_disk_service_version(wallet_root) if wallet_root is not None else None
189
+ )
190
+ running_version = str(health.get("version") or "").strip()
191
+ if expected_version is None or running_version == expected_version:
192
+ return
193
+ _stop_local_service(service_url)
85
194
  if not _is_local_service_url(service_url):
86
195
  raise WalletBackendError(
87
196
  f"wdk-evm-wallet is unreachable at {_health_url(service_url)} and auto-start only supports localhost URLs."
88
197
  )
89
- wallet_root = _resolve_local_wdk_evm_root()
90
198
  if wallet_root is None:
91
199
  raise WalletBackendError(
92
200
  "wdk-evm-wallet is not healthy and the local launcher could not be found."
@@ -476,6 +476,26 @@ class OpenClawWalletAdapter:
476
476
  "evm_swap_fingerprint": evm_swap_fingerprint,
477
477
  }
478
478
 
479
+ if asset_type == "evm-uniswap-swap":
480
+ provided_fingerprint = payload.get("quote_fingerprint")
481
+ return {
482
+ "operation": action_label,
483
+ "network": str(payload.get("network") or getattr(self.backend, "network", "unknown")),
484
+ "wallet": payload.get("wallet"),
485
+ "from_address": payload.get("from_address"),
486
+ "swap_provider": payload.get("swap_provider"),
487
+ "routing": payload.get("routing"),
488
+ "token_in": payload.get("token_in"),
489
+ "token_out": payload.get("token_out"),
490
+ "input_amount_raw": payload.get("input_amount_raw"),
491
+ "estimated_output_amount_raw": payload.get("estimated_output_amount_raw"),
492
+ "minimum_output_amount_raw": payload.get("minimum_output_amount_raw"),
493
+ "slippage_bps": payload.get("slippage_bps"),
494
+ "permit_required": payload.get("permit_required"),
495
+ "router": payload.get("router"),
496
+ "quote_fingerprint": provided_fingerprint,
497
+ }
498
+
479
499
  if asset_type == "evm-aave-v3":
480
500
  return {
481
501
  "operation": action_label,
@@ -804,6 +824,13 @@ class OpenClawWalletAdapter:
804
824
  is_mainnet = self._is_mainnet_network(network)
805
825
  annotated["network"] = network
806
826
  annotated["is_mainnet"] = is_mainnet
827
+ # Stamp the mode so the host bridge can cache preview/prepare responses for
828
+ # auto-approval at execute. The bridge already treats "preview" as cacheable
829
+ # (alongside "prepare"/"intent_preview"); without this field a preview -> execute
830
+ # flow never caches and execute fails with "confirmation context expired",
831
+ # regardless of timing. prepare carries its own mode via _build_prepare_plan;
832
+ # "execute" responses stay uncached (not in the bridge's cacheable set).
833
+ annotated["mode"] = mode
807
834
  annotated["confirmation_summary"] = self._build_confirmation_summary(
808
835
  action_label=action_label,
809
836
  payload=annotated,
@@ -1587,6 +1614,131 @@ class OpenClawWalletAdapter:
1587
1614
  risk_level="high",
1588
1615
  ),
1589
1616
  )
1617
+ tools.insert(
1618
+ 11,
1619
+ AgentToolSpec(
1620
+ name="get_uniswap_swap_quote",
1621
+ description=(
1622
+ "Get a read-only Uniswap Trading API quote (CLASSIC routing) for an ERC-20 or native ETH swap "
1623
+ "on ethereum or base. Supports full EIP-712 Permit2 path for ERC-20 inputs. "
1624
+ "This does not approve, sign, or execute a swap."
1625
+ ),
1626
+ input_schema={
1627
+ "type": "object",
1628
+ "properties": {
1629
+ "token_in": {
1630
+ "type": "string",
1631
+ "description": "ERC-20 contract address for the input token, or native/eth for native ETH.",
1632
+ },
1633
+ "token_out": {
1634
+ "type": "string",
1635
+ "description": "ERC-20 contract address for the output token, or native/eth for native ETH.",
1636
+ },
1637
+ "amount_in_raw": {
1638
+ "type": "string",
1639
+ "description": "Input amount in token base units as a base-10 integer string.",
1640
+ },
1641
+ "slippage_bps": {
1642
+ "type": "integer",
1643
+ "description": "Optional slippage tolerance in basis points (e.g. 50 = 0.5%, 300 = 3%). Defaults to 300.",
1644
+ },
1645
+ "network": {
1646
+ "type": "string",
1647
+ "enum": ["ethereum", "base"],
1648
+ "description": "EVM network. Uniswap Trading API supports ethereum and base only.",
1649
+ },
1650
+ },
1651
+ "required": ["token_in", "token_out", "amount_in_raw"],
1652
+ "additionalProperties": False,
1653
+ },
1654
+ read_only=True,
1655
+ risk_level="low",
1656
+ ),
1657
+ )
1658
+ tools.insert(
1659
+ 12,
1660
+ AgentToolSpec(
1661
+ name="swap_evm_uniswap_tokens",
1662
+ description=(
1663
+ "Preview, prepare, or execute an ERC-20 or native ETH swap through the Uniswap Trading API "
1664
+ "(CLASSIC routing) on ethereum or base. ERC-20 inputs use Permit2 EIP-712 signing automatically. "
1665
+ "Prepare returns an execution plan only. Execute requires a host-issued approval token bound to the previewed operation."
1666
+ ),
1667
+ input_schema={
1668
+ "type": "object",
1669
+ "properties": {
1670
+ "token_in": {
1671
+ "type": "string",
1672
+ "description": "ERC-20 contract address or native/eth for native ETH.",
1673
+ },
1674
+ "token_out": {
1675
+ "type": "string",
1676
+ "description": "ERC-20 contract address or native/eth for native ETH.",
1677
+ },
1678
+ "amount_in_raw": {
1679
+ "type": "string",
1680
+ "description": "Input amount in token base units as a base-10 integer string.",
1681
+ },
1682
+ "slippage_bps": {
1683
+ "type": "integer",
1684
+ "description": "Optional slippage tolerance in basis points (e.g. 50 = 0.5%, 300 = 3%). Defaults to 300.",
1685
+ },
1686
+ "mode": {
1687
+ "type": "string",
1688
+ "enum": ["preview", "prepare", "execute"],
1689
+ },
1690
+ "purpose": {"type": "string"},
1691
+ "user_intent": {"type": "boolean"},
1692
+ "approval_token": {"type": "string"},
1693
+ "network": {
1694
+ "type": "string",
1695
+ "enum": ["ethereum", "base"],
1696
+ "description": "EVM network. Uniswap Trading API supports ethereum and base only.",
1697
+ },
1698
+ },
1699
+ "required": ["token_in", "token_out", "amount_in_raw", "mode", "purpose"],
1700
+ "additionalProperties": False,
1701
+ },
1702
+ read_only=False,
1703
+ requires_explicit_user_intent=True,
1704
+ risk_level="high",
1705
+ ),
1706
+ )
1707
+ tools.insert(
1708
+ 13,
1709
+ AgentToolSpec(
1710
+ name="issue_wallet_approval",
1711
+ description=(
1712
+ "Issue a host approval token bound to an exact wallet operation. "
1713
+ "Call this after prepare mode to get the token required for execute mode. "
1714
+ "Pass tool_name and the confirmation_summary exactly as returned in the prepare response. "
1715
+ "In Claude Code this is the bridge step that Codex performs automatically via its UI dialog."
1716
+ ),
1717
+ input_schema={
1718
+ "type": "object",
1719
+ "properties": {
1720
+ "tool_name": {
1721
+ "type": "string",
1722
+ "description": "Name of the tool being approved, e.g. swap_evm_uniswap_tokens or swap_evm_tokens.",
1723
+ },
1724
+ "summary": {
1725
+ "type": "object",
1726
+ "description": "The confirmation_summary dict returned verbatim from the prepare response.",
1727
+ "additionalProperties": True,
1728
+ },
1729
+ "mainnet_confirmed": {
1730
+ "type": "boolean",
1731
+ "description": "Set to true to confirm this is a mainnet operation and you accept the risk.",
1732
+ },
1733
+ },
1734
+ "required": ["tool_name", "summary", "mainnet_confirmed"],
1735
+ "additionalProperties": False,
1736
+ },
1737
+ read_only=False,
1738
+ requires_explicit_user_intent=True,
1739
+ risk_level="high",
1740
+ ),
1741
+ )
1590
1742
 
1591
1743
  return tools
1592
1744
 
@@ -3753,6 +3905,185 @@ class OpenClawWalletAdapter:
3753
3905
  ),
3754
3906
  )
3755
3907
 
3908
+ if tool_name == "get_uniswap_swap_quote":
3909
+ token_in = args.get("token_in")
3910
+ token_out = args.get("token_out")
3911
+ amount_in_raw = args.get("amount_in_raw")
3912
+ slippage_bps = args.get("slippage_bps")
3913
+ if not isinstance(token_in, str) or not token_in.strip():
3914
+ raise WalletBackendError("token_in is required.")
3915
+ if not isinstance(token_out, str) or not token_out.strip():
3916
+ raise WalletBackendError("token_out is required.")
3917
+ if not isinstance(amount_in_raw, str) or not amount_in_raw.strip().isdigit():
3918
+ raise WalletBackendError("amount_in_raw must be a positive integer string.")
3919
+ if int(amount_in_raw.strip()) <= 0:
3920
+ raise WalletBackendError("amount_in_raw must be greater than zero.")
3921
+ if slippage_bps is not None and (not isinstance(slippage_bps, int) or slippage_bps < 0 or slippage_bps > 5000):
3922
+ raise WalletBackendError("slippage_bps must be an integer between 0 and 5000.")
3923
+ data = await active_backend.get_uniswap_swap_quote(
3924
+ token_in=token_in.strip(),
3925
+ token_out=token_out.strip(),
3926
+ amount_in_raw=amount_in_raw.strip(),
3927
+ slippage_bps=slippage_bps,
3928
+ )
3929
+ return AgentToolResult(tool=tool_name, ok=True, data=data)
3930
+
3931
+ if tool_name == "swap_evm_uniswap_tokens":
3932
+ token_in = args.get("token_in")
3933
+ token_out = args.get("token_out")
3934
+ amount_in_raw = args.get("amount_in_raw")
3935
+ slippage_bps = args.get("slippage_bps")
3936
+ mode = args.get("mode")
3937
+ purpose = args.get("purpose")
3938
+ user_intent = args.get("user_intent", False)
3939
+ approval_token = args.get("approval_token")
3940
+
3941
+ if not isinstance(token_in, str) or not token_in.strip():
3942
+ raise WalletBackendError("token_in is required.")
3943
+ if not isinstance(token_out, str) or not token_out.strip():
3944
+ raise WalletBackendError("token_out is required.")
3945
+ if not isinstance(amount_in_raw, str) or not amount_in_raw.strip().isdigit():
3946
+ raise WalletBackendError("amount_in_raw must be a positive integer string.")
3947
+ if int(amount_in_raw.strip()) <= 0:
3948
+ raise WalletBackendError("amount_in_raw must be greater than zero.")
3949
+ if mode not in {"preview", "prepare", "execute"}:
3950
+ raise WalletBackendError("mode must be 'preview', 'prepare' or 'execute'.")
3951
+ if not isinstance(purpose, str) or not purpose.strip():
3952
+ raise WalletBackendError("purpose is required.")
3953
+ if slippage_bps is not None and (not isinstance(slippage_bps, int) or slippage_bps < 0 or slippage_bps > 5000):
3954
+ raise WalletBackendError("slippage_bps must be an integer between 0 and 5000.")
3955
+
3956
+ preview_kwargs = {
3957
+ "token_in": token_in.strip(),
3958
+ "token_out": token_out.strip(),
3959
+ "amount_in_raw": amount_in_raw.strip(),
3960
+ "slippage_bps": slippage_bps,
3961
+ }
3962
+
3963
+ if mode == "preview":
3964
+ preview = await active_backend.preview_uniswap_swap(**preview_kwargs)
3965
+ return AgentToolResult(
3966
+ tool=tool_name,
3967
+ ok=True,
3968
+ data=self._annotate_sensitive_payload(
3969
+ preview,
3970
+ action_label="Uniswap swap",
3971
+ mode="preview",
3972
+ ),
3973
+ )
3974
+
3975
+ if mode == "prepare":
3976
+ self._require_prepare_intent(user_intent)
3977
+ preview = await active_backend.preview_uniswap_swap(**preview_kwargs)
3978
+ return AgentToolResult(
3979
+ tool=tool_name,
3980
+ ok=True,
3981
+ data=self._annotate_sensitive_payload(
3982
+ self._build_prepare_plan(
3983
+ preview_payload=preview,
3984
+ action_label="Uniswap swap",
3985
+ ),
3986
+ action_label="Uniswap swap",
3987
+ mode="prepare",
3988
+ ),
3989
+ )
3990
+
3991
+ approval_payload = inspect_approval_token(
3992
+ approval_token,
3993
+ tool_name=tool_name,
3994
+ network=str(getattr(active_backend, "network", "unknown")),
3995
+ require_mainnet_confirmation=self._is_mainnet_for_backend(active_backend),
3996
+ )
3997
+ approval_summary = approval_payload.get("binding", {}).get("summary")
3998
+ if not isinstance(approval_summary, dict):
3999
+ raise WalletBackendError(
4000
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4001
+ )
4002
+ expected_summary = {
4003
+ "operation": "Uniswap swap",
4004
+ "network": str(getattr(active_backend, "network", "unknown")),
4005
+ "token_in": token_in.strip(),
4006
+ "token_out": token_out.strip(),
4007
+ "input_amount_raw": amount_in_raw.strip(),
4008
+ }
4009
+ for key, expected_value in expected_summary.items():
4010
+ if approval_summary.get(key) != expected_value:
4011
+ raise WalletBackendError(
4012
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4013
+ )
4014
+
4015
+ approval_summary_copy = dict(approval_summary)
4016
+ self._require_execute_approval(
4017
+ approval_token=approval_token,
4018
+ tool_name=tool_name,
4019
+ summary=approval_summary_copy,
4020
+ action_label="Uniswap swap",
4021
+ backend=active_backend,
4022
+ )
4023
+ bound_quote_fingerprint = approval_summary_copy.get("quote_fingerprint")
4024
+ bound_minimum_output_amount_raw = approval_summary_copy.get("minimum_output_amount_raw")
4025
+ result = await active_backend.send_uniswap_swap(
4026
+ **preview_kwargs,
4027
+ expected_quote_fingerprint=(
4028
+ bound_quote_fingerprint.strip()
4029
+ if isinstance(bound_quote_fingerprint, str) and bound_quote_fingerprint.strip()
4030
+ else None
4031
+ ),
4032
+ minimum_output_amount_raw=(
4033
+ str(bound_minimum_output_amount_raw).strip()
4034
+ if bound_minimum_output_amount_raw is not None
4035
+ else None
4036
+ ),
4037
+ )
4038
+ return AgentToolResult(
4039
+ tool=tool_name,
4040
+ ok=True,
4041
+ data=self._annotate_sensitive_payload(
4042
+ result,
4043
+ action_label="Uniswap swap",
4044
+ mode="execute",
4045
+ ),
4046
+ )
4047
+
4048
+ if tool_name == "issue_wallet_approval":
4049
+ from agent_wallet.approval import issue_approval_token
4050
+
4051
+ target_tool = args.get("tool_name")
4052
+ summary = args.get("summary")
4053
+ mainnet_confirmed = args.get("mainnet_confirmed", False)
4054
+
4055
+ if not isinstance(target_tool, str) or not target_tool.strip():
4056
+ raise WalletBackendError("tool_name is required.")
4057
+ if not isinstance(summary, dict) or not summary:
4058
+ raise WalletBackendError("summary must be a non-empty object matching the prepare response confirmation_summary.")
4059
+ if not isinstance(mainnet_confirmed, bool):
4060
+ raise WalletBackendError("mainnet_confirmed must be a boolean.")
4061
+ if self._is_mainnet_for_backend(active_backend) and not mainnet_confirmed:
4062
+ raise WalletBackendError(
4063
+ "mainnet_confirmed must be true for mainnet operations. "
4064
+ "Set mainnet_confirmed: true to confirm you accept the risk of an irreversible mainnet transaction."
4065
+ )
4066
+
4067
+ network = str(getattr(active_backend, "network", "unknown"))
4068
+ token = issue_approval_token(
4069
+ tool_name=target_tool.strip(),
4070
+ network=network,
4071
+ summary=summary,
4072
+ mainnet_confirmed=mainnet_confirmed,
4073
+ issued_by="claude-code-bridge",
4074
+ )
4075
+ return AgentToolResult(
4076
+ tool=tool_name,
4077
+ ok=True,
4078
+ data={
4079
+ "approval_token": token,
4080
+ "tool_name": target_tool.strip(),
4081
+ "network": network,
4082
+ "mainnet_confirmed": mainnet_confirmed,
4083
+ "usage": f"Pass approval_token to {target_tool.strip()} with mode='execute'.",
4084
+ },
4085
+ )
4086
+
3756
4087
  if tool_name == "get_wallet_portfolio":
3757
4088
  address = args.get("address")
3758
4089
  if address is not None and not isinstance(address, str):
@@ -26,6 +26,8 @@ LONG_RUNNING_POST_PATHS = {
26
26
  "/v1/evm/lido/claim_withdrawal/send",
27
27
  "/v1/evm/swap/quote",
28
28
  "/v1/evm/swap/send",
29
+ "/v1/evm/uniswap/swap/quote",
30
+ "/v1/evm/uniswap/swap/send",
29
31
  "/v1/evm/lifi/quote",
30
32
  "/v1/evm/lifi/send",
31
33
  "/v1/evm/transfer/send",
@@ -226,6 +226,38 @@ class AgentWalletBackend(ABC):
226
226
  ) -> dict[str, Any]:
227
227
  raise WalletBackendError(f"{self.name} does not support EVM LI.FI cross-chain swaps.")
228
228
 
229
+ async def get_uniswap_swap_quote(
230
+ self,
231
+ *,
232
+ token_in: str,
233
+ token_out: str,
234
+ amount_in_raw: str,
235
+ slippage_bps: int | None = None,
236
+ ) -> dict[str, Any]:
237
+ raise WalletBackendError(f"{self.name} does not support Uniswap swap quotes.")
238
+
239
+ async def preview_uniswap_swap(
240
+ self,
241
+ *,
242
+ token_in: str,
243
+ token_out: str,
244
+ amount_in_raw: str,
245
+ slippage_bps: int | None = None,
246
+ ) -> dict[str, Any]:
247
+ raise WalletBackendError(f"{self.name} does not support Uniswap swap previews.")
248
+
249
+ async def send_uniswap_swap(
250
+ self,
251
+ *,
252
+ token_in: str,
253
+ token_out: str,
254
+ amount_in_raw: str,
255
+ slippage_bps: int | None = None,
256
+ expected_quote_fingerprint: str | None = None,
257
+ minimum_output_amount_raw: str | None = None,
258
+ ) -> dict[str, Any]:
259
+ raise WalletBackendError(f"{self.name} does not support Uniswap swaps.")
260
+
229
261
  async def preview_evm_native_transfer(
230
262
  self,
231
263
  *,