@agentlayer.tech/wallet 0.1.37 → 0.1.44

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.
Files changed (34) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
  3. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  4. package/CHANGELOG.md +37 -0
  5. package/README.md +14 -0
  6. package/VERSION +1 -1
  7. package/agent-wallet/agent_wallet/__init__.py +1 -1
  8. package/agent-wallet/agent_wallet/evm_user_wallets.py +114 -6
  9. package/agent-wallet/agent_wallet/openclaw_adapter.py +331 -0
  10. package/agent-wallet/agent_wallet/openclaw_cli.py +22 -0
  11. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +2 -0
  12. package/agent-wallet/agent_wallet/telemetry.py +306 -0
  13. package/agent-wallet/agent_wallet/wallet_layer/base.py +32 -0
  14. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +156 -0
  15. package/agent-wallet/openclaw.plugin.json +1 -1
  16. package/agent-wallet/pyproject.toml +1 -1
  17. package/agent-wallet/scripts/bootstrap_openclaw_evm.py +20 -2
  18. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  19. package/claude-code/plugins/agent-wallet/README.md +30 -1
  20. package/claude-code/plugins/agent-wallet/commands/wallet-setup.md +24 -0
  21. package/claude-code/plugins/agent-wallet/hooks/hooks.json +16 -0
  22. package/claude-code/plugins/agent-wallet/scripts/bootstrap_backend.sh +136 -0
  23. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +6 -1
  24. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  25. package/codex/plugins/agent-wallet/scripts/run_mcp.sh +5 -0
  26. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  27. package/hermes/plugins/agent_wallet/tools.py +3 -0
  28. package/package.json +2 -1
  29. package/wdk-btc-wallet/package.json +1 -1
  30. package/wdk-evm-wallet/README.md +25 -0
  31. package/wdk-evm-wallet/package.json +4 -2
  32. package/wdk-evm-wallet/src/config.js +47 -0
  33. package/wdk-evm-wallet/src/server.js +25 -3
  34. package/wdk-evm-wallet/src/wdk_evm_wallet.js +545 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "agentlayer",
3
+ "description": "AgentLayer plugins for Claude Code. Install the wallet bridge and its backend runtime without leaving the CLI.",
4
+ "owner": {
5
+ "name": "AgentLayer",
6
+ "url": "https://github.com/lopushok9/Agent-Layer"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "agent-wallet",
11
+ "displayName": "Agent Wallet",
12
+ "description": "Bridge to the local AgentLayer wallet runtime (Solana, Bitcoin, EVM). Installs and connects the backend runtime on first use.",
13
+ "category": "development",
14
+ "source": "./claude-code/plugins/agent-wallet"
15
+ }
16
+ ]
17
+ }
@@ -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.37",
5
+ "version": "0.1.44",
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.37",
3
+ "version": "0.1.44",
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.44 - 2026-06-12
6
+
7
+ - Added anonymous, privacy-first adoption telemetry so wallet usage can be
8
+ measured across hosts (Claude Code / Codex / Hermes / OpenClaw) without any
9
+ PII.
10
+ - The wallet emits one event per tool invocation through the shared
11
+ `openclaw_cli invoke` chokepoint. Events carry only a random local install
12
+ id, host, the registered tool name, backend family, plugin version, and a
13
+ success flag — never addresses, balances, amounts, tx hashes, arguments, or
14
+ secrets. Secret-touching commands (onboard/wallet-create/unlock/import) are
15
+ never instrumented.
16
+ - Delivery uses a durable local spool plus a detached best-effort flush to the
17
+ provider-gateway, so a short-lived CLI process never adds latency or loses
18
+ events.
19
+ - Opt out at any time with `AGENT_WALLET_NO_TELEMETRY=1` (zero footprint).
20
+ - Per-frontend `host` tagging via `AGENT_WALLET_HOST` in each bridge.
21
+
22
+ ## v0.1.41 - 2026-06-07
23
+
24
+ - Hardened Uniswap Trading API swap execution on active EVM mainnet markets.
25
+ - Stabilized the Uniswap quote fingerprint so `preview -> execute` binds to
26
+ the swap intent rather than a per-block quoted output amount, preventing
27
+ harmless repricing from failing execute with `swap_quote_changed`.
28
+ - Raised the default Uniswap slippage floor from `50` bps to `300` bps
29
+ (`3%`) so normal drift during the approval/execution window on Base no
30
+ longer causes avoidable failures. Per-call and env overrides still apply.
31
+ - Made the local `wdk-evm-wallet` daemon version-aware and self-refreshing.
32
+ - `/health` now reports the real launcher version instead of a hardcoded
33
+ `0.1.0`.
34
+ - Local EVM autostart compares the running daemon version against the on-disk
35
+ launcher version and automatically restarts a stale daemon after local
36
+ release/install, avoiding old code staying resident in memory.
37
+ - Extended the long-running EVM HTTP timeout policy to the Uniswap
38
+ `/v1/evm/uniswap/swap/{quote,send}` routes so approve+swap flows on Base do
39
+ not time out client-side while the daemon is still finishing the on-chain
40
+ operation.
41
+
5
42
  ## v0.1.37 - 2026-06-05
6
43
 
7
44
  - Fixed the Claude Code / Codex MCP bridge failing to start with
package/README.md CHANGED
@@ -18,6 +18,20 @@ For Codex:
18
18
  npx @agentlayer.tech/wallet install --yes && npx @agentlayer.tech/wallet codex install --yes
19
19
  ```
20
20
 
21
+ For Claude Code:
22
+
23
+ ```bash
24
+ npx @agentlayer.tech/wallet install --yes && npx @agentlayer.tech/wallet claude-code install --yes
25
+ ```
26
+
27
+ Or install entirely from inside the Claude Code CLI, via the plugin marketplace
28
+ (no terminal/npx needed) — two commands, then restart:
29
+
30
+ ```text
31
+ /plugin marketplace add lopushok9/Agent-Layer
32
+ /plugin install agent-wallet@agentlayer
33
+ ```
34
+
21
35
  For Hermes:
22
36
 
23
37
  ```bash
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.37
1
+ 0.1.44
@@ -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.37"
5
+ __version__ = "0.1.44"
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):
@@ -11,6 +11,12 @@ from typing import Any
11
11
 
12
12
  from agent_wallet.wallet_layer.base import WalletBackendError
13
13
 
14
+ try: # telemetry is optional and must never break the CLI
15
+ from agent_wallet.telemetry import record as _telemetry_record
16
+ except Exception: # pragma: no cover - defensive
17
+ def _telemetry_record(*_args: Any, **_kwargs: Any) -> None:
18
+ return None
19
+
14
20
 
15
21
  def _parse_bool(value: Any) -> str:
16
22
  if value is None:
@@ -610,6 +616,15 @@ def main() -> int:
610
616
  )
611
617
  )
612
618
  except Exception as exc:
619
+ # Anonymous adoption telemetry for tool invocations only (never for
620
+ # onboard/wallet-create/unlock/import — those touch secrets). Records
621
+ # just the tool name + backend family + failure flag; never raises.
622
+ if getattr(args, "command", "") == "invoke":
623
+ _telemetry_record(
624
+ getattr(args, "tool", ""),
625
+ backend=str(locals().get("config", {}).get("backend", "") or ""),
626
+ ok=False,
627
+ )
613
628
  error_payload: dict[str, Any] = {"ok": False, "error": str(exc)}
614
629
  if isinstance(exc, WalletBackendError):
615
630
  if exc.code:
@@ -619,6 +634,13 @@ def main() -> int:
619
634
  print(json.dumps(error_payload), file=sys.stderr)
620
635
  return 1
621
636
 
637
+ if getattr(args, "command", "") == "invoke":
638
+ _telemetry_record(
639
+ getattr(args, "tool", ""),
640
+ backend=str(config.get("backend", "") or ""),
641
+ ok=True,
642
+ )
643
+
622
644
  print(json.dumps(payload))
623
645
  return 0
624
646