@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.
- 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 +8 -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/providers/wdk_evm_local.py +2 -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/scripts/run_mcp.sh +14 -7
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +3 -2
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -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
|
@@ -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.43",
|
|
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.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
|
[](https://docs.agent-layer.tech/)
|
|
7
7
|
[](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.
|
|
1
|
+
0.1.43
|
|
@@ -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):
|
|
@@ -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
|
*,
|