@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.
- package/.claude-plugin/marketplace.json +17 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +37 -0
- package/README.md +14 -0
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/evm_user_wallets.py +114 -6
- package/agent-wallet/agent_wallet/openclaw_adapter.py +331 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +22 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +2 -0
- package/agent-wallet/agent_wallet/telemetry.py +306 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +32 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +156 -0
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +20 -2
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/README.md +30 -1
- package/claude-code/plugins/agent-wallet/commands/wallet-setup.md +24 -0
- package/claude-code/plugins/agent-wallet/hooks/hooks.json +16 -0
- package/claude-code/plugins/agent-wallet/scripts/bootstrap_backend.sh +136 -0
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +6 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +5 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/hermes/plugins/agent_wallet/tools.py +3 -0
- package/package.json +2 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/README.md +25 -0
- package/wdk-evm-wallet/package.json +4 -2
- package/wdk-evm-wallet/src/config.js +47 -0
- package/wdk-evm-wallet/src/server.js +25 -3
- 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.
|
|
5
|
+
"version": "0.1.44",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"close_empty_token_accounts",
|
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.
|
|
1
|
+
0.1.44
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|