@agentlayer.tech/wallet 0.1.88 → 0.1.89
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 +2 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +92 -0
- package/agent-wallet/agent_wallet/providers/dexscreener.py +145 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +12 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +38 -1
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- 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/package.json +1 -1
|
@@ -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.89",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"agentlayer_autonomous_approve",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"manage_evm_morpho_vault_position",
|
|
62
62
|
"manage_evm_lido_position",
|
|
63
63
|
"manage_evm_lido_withdrawal",
|
|
64
|
+
"search_uniswap_pairs",
|
|
64
65
|
"set_evm_network",
|
|
65
66
|
"set_wallet_backend",
|
|
66
67
|
"sign_wallet_message",
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.89
|
|
@@ -2211,6 +2211,67 @@ class OpenClawWalletAdapter:
|
|
|
2211
2211
|
risk_level="low",
|
|
2212
2212
|
),
|
|
2213
2213
|
)
|
|
2214
|
+
tools.append(
|
|
2215
|
+
AgentToolSpec(
|
|
2216
|
+
name="search_uniswap_pairs",
|
|
2217
|
+
description=(
|
|
2218
|
+
"Search for onchain Uniswap-tradeable token pairs by ticker/name or by exact "
|
|
2219
|
+
"ERC-20 address, with live price, 5m/1h/6h/24h volume, liquidity, FDV, market "
|
|
2220
|
+
"cap, and buy/sell counts. Backed by DexScreener's public index (covers "
|
|
2221
|
+
"Uniswap v2/v3/v4 pools and other DEXes on the same chain), since the Uniswap "
|
|
2222
|
+
"Trading API itself only quotes a pair you already know the addresses for, not "
|
|
2223
|
+
"search-by-name. Defaults to the currently active EVM network (e.g. robinhood); "
|
|
2224
|
+
"set all_chains to search across every chain DexScreener indexes. Read-only "
|
|
2225
|
+
"market data, not a swap quote. Free-text search can surface impersonator "
|
|
2226
|
+
"tokens reusing a legitimate ticker with fabricated liquidity/FDV — before "
|
|
2227
|
+
"quoting or swapping, verify the token_address independently (e.g. via "
|
|
2228
|
+
"get_evm_token_metadata) rather than trusting the top result by symbol match. "
|
|
2229
|
+
"Do not use liquidity/FDV size to pick a winner: observed impersonators on "
|
|
2230
|
+
"robinhood fabricate liquidity/FDV an order of magnitude above the genuine "
|
|
2231
|
+
"pool specifically to look most trustworthy, and a Uniswap quote succeeding "
|
|
2232
|
+
"for a token proves nothing either (Trading API routes any pool with reserves, "
|
|
2233
|
+
"impersonators included). This only matters for tickers claiming to represent "
|
|
2234
|
+
"a real-world asset (stocks/ETFs) - unrelated meme/community tokens on the "
|
|
2235
|
+
"same chain aren't impersonators just for lacking a canonical issuer. When the "
|
|
2236
|
+
"query is a real-world-asset ticker, cross-check the returned token_address "
|
|
2237
|
+
"against Robinhood's own canonical list at docs.robinhood.com/chain/contracts "
|
|
2238
|
+
"(or the equivalent official source for other chains) before relying on the result."
|
|
2239
|
+
),
|
|
2240
|
+
input_schema={
|
|
2241
|
+
"type": "object",
|
|
2242
|
+
"properties": {
|
|
2243
|
+
"query": {
|
|
2244
|
+
"type": "string",
|
|
2245
|
+
"description": "Free-text search, e.g. a ticker or name (\"AAPL\", \"Apple Robinhood\"). Required unless token_address is given.",
|
|
2246
|
+
},
|
|
2247
|
+
"token_address": {
|
|
2248
|
+
"type": "string",
|
|
2249
|
+
"description": "Exact ERC-20 contract address to look up instead of a free-text query.",
|
|
2250
|
+
},
|
|
2251
|
+
"chain": {
|
|
2252
|
+
"type": "string",
|
|
2253
|
+
"description": "DexScreener chain slug to filter to (e.g. \"robinhood\", \"ethereum\", \"base\"). Defaults to the active EVM network.",
|
|
2254
|
+
},
|
|
2255
|
+
"dex_id": {
|
|
2256
|
+
"type": "string",
|
|
2257
|
+
"description": "Optional exact DEX filter, e.g. \"uniswap\".",
|
|
2258
|
+
},
|
|
2259
|
+
"all_chains": {
|
|
2260
|
+
"type": "boolean",
|
|
2261
|
+
"description": "If true, do not filter query results to a single chain. Ignored when token_address is set.",
|
|
2262
|
+
},
|
|
2263
|
+
"limit": {
|
|
2264
|
+
"type": "integer",
|
|
2265
|
+
"description": "Max number of pairs to return (1-30). Defaults to 10.",
|
|
2266
|
+
},
|
|
2267
|
+
},
|
|
2268
|
+
"required": [],
|
|
2269
|
+
"additionalProperties": False,
|
|
2270
|
+
},
|
|
2271
|
+
read_only=True,
|
|
2272
|
+
risk_level="low",
|
|
2273
|
+
),
|
|
2274
|
+
)
|
|
2214
2275
|
tools.insert(
|
|
2215
2276
|
12,
|
|
2216
2277
|
AgentToolSpec(
|
|
@@ -5460,6 +5521,37 @@ class OpenClawWalletAdapter:
|
|
|
5460
5521
|
)
|
|
5461
5522
|
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
5462
5523
|
|
|
5524
|
+
if tool_name == "search_uniswap_pairs":
|
|
5525
|
+
query = args.get("query")
|
|
5526
|
+
token_address = args.get("token_address")
|
|
5527
|
+
chain = args.get("chain")
|
|
5528
|
+
dex_id = args.get("dex_id")
|
|
5529
|
+
all_chains = bool(args.get("all_chains", False))
|
|
5530
|
+
limit = args.get("limit", 10)
|
|
5531
|
+
if query is not None and not isinstance(query, str):
|
|
5532
|
+
raise WalletBackendError("query must be a string.")
|
|
5533
|
+
if token_address is not None and not isinstance(token_address, str):
|
|
5534
|
+
raise WalletBackendError("token_address must be a string.")
|
|
5535
|
+
if not (isinstance(query, str) and query.strip()) and not (
|
|
5536
|
+
isinstance(token_address, str) and token_address.strip()
|
|
5537
|
+
):
|
|
5538
|
+
raise WalletBackendError("Either query or token_address is required.")
|
|
5539
|
+
if chain is not None and not isinstance(chain, str):
|
|
5540
|
+
raise WalletBackendError("chain must be a string.")
|
|
5541
|
+
if dex_id is not None and not isinstance(dex_id, str):
|
|
5542
|
+
raise WalletBackendError("dex_id must be a string.")
|
|
5543
|
+
if not isinstance(limit, int) or limit < 1 or limit > 30:
|
|
5544
|
+
raise WalletBackendError("limit must be an integer between 1 and 30.")
|
|
5545
|
+
data = await active_backend.search_uniswap_pairs(
|
|
5546
|
+
query=query,
|
|
5547
|
+
token_address=token_address,
|
|
5548
|
+
chain=chain,
|
|
5549
|
+
dex_id=dex_id,
|
|
5550
|
+
all_chains=all_chains,
|
|
5551
|
+
limit=limit,
|
|
5552
|
+
)
|
|
5553
|
+
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
5554
|
+
|
|
5463
5555
|
if tool_name == "swap_evm_uniswap_tokens":
|
|
5464
5556
|
token_in = args.get("token_in")
|
|
5465
5557
|
token_out = args.get("token_out")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""DexScreener provider for onchain token/pair discovery (price, volume, liquidity).
|
|
2
|
+
|
|
3
|
+
Public API, no key required: https://docs.dexscreener.com/api/reference. Used to
|
|
4
|
+
answer "which pairs exist for this token/ticker" the way app.uniswap.org's own
|
|
5
|
+
search box does, since the Uniswap Trading API itself only exposes quote/order/swap
|
|
6
|
+
for a pair you already know, not a search-by-name/address endpoint.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from agent_wallet.exceptions import ProviderError
|
|
15
|
+
from agent_wallet.http_client import get_client
|
|
16
|
+
|
|
17
|
+
DEXSCREENER_BASE_URL = "https://api.dexscreener.com"
|
|
18
|
+
|
|
19
|
+
# DexScreener's own responses are served with `cache-control: public, max-age=30`;
|
|
20
|
+
# mirroring that TTL avoids redundant lookups within a session without serving
|
|
21
|
+
# data staler than DexScreener itself would.
|
|
22
|
+
_CACHE_TTL_SECONDS = 30
|
|
23
|
+
_CACHE_MAX_ENTRIES = 128
|
|
24
|
+
_cache: dict[str, tuple[float, list[dict[str, Any]]]] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _cache_get(key: str) -> list[dict[str, Any]] | None:
|
|
28
|
+
entry = _cache.get(key)
|
|
29
|
+
if entry is None:
|
|
30
|
+
return None
|
|
31
|
+
stored_at, pairs = entry
|
|
32
|
+
if time.monotonic() - stored_at > _CACHE_TTL_SECONDS:
|
|
33
|
+
_cache.pop(key, None)
|
|
34
|
+
return None
|
|
35
|
+
return pairs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _cache_set(key: str, pairs: list[dict[str, Any]]) -> None:
|
|
39
|
+
if len(_cache) >= _CACHE_MAX_ENTRIES and key not in _cache:
|
|
40
|
+
oldest_key = min(_cache, key=lambda existing: _cache[existing][0])
|
|
41
|
+
_cache.pop(oldest_key, None)
|
|
42
|
+
_cache[key] = (time.monotonic(), pairs)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _get(path: str, *, params: dict[str, Any] | None = None) -> Any:
|
|
46
|
+
client = get_client()
|
|
47
|
+
response = await client.get(f"{DEXSCREENER_BASE_URL}{path}", params=params)
|
|
48
|
+
if response.status_code != 200:
|
|
49
|
+
raise ProviderError("dexscreener", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
50
|
+
try:
|
|
51
|
+
return response.json()
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
raise ProviderError("dexscreener", "Unexpected non-JSON response from DexScreener.") from exc
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _extract_pairs(data: Any) -> list[dict[str, Any]]:
|
|
57
|
+
if isinstance(data, dict):
|
|
58
|
+
pairs = data.get("pairs")
|
|
59
|
+
return pairs if isinstance(pairs, list) else []
|
|
60
|
+
if isinstance(data, list):
|
|
61
|
+
return [item for item in data if isinstance(item, dict)]
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def search_pairs(query: str) -> list[dict[str, Any]]:
|
|
66
|
+
"""Free-text search across every chain/DEX DexScreener indexes."""
|
|
67
|
+
query = query.strip()
|
|
68
|
+
if not query:
|
|
69
|
+
raise ProviderError("dexscreener", "query must not be empty.")
|
|
70
|
+
cache_key = f"search:{query.lower()}"
|
|
71
|
+
cached = _cache_get(cache_key)
|
|
72
|
+
if cached is not None:
|
|
73
|
+
return cached
|
|
74
|
+
data = await _get("/latest/dex/search", params={"q": query})
|
|
75
|
+
pairs = _extract_pairs(data)
|
|
76
|
+
_cache_set(cache_key, pairs)
|
|
77
|
+
return pairs
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def get_pairs_for_token(*, chain: str, token_address: str) -> list[dict[str, Any]]:
|
|
81
|
+
"""All pairs for a single token address on one chain, across every DEX on it."""
|
|
82
|
+
chain = chain.strip().lower()
|
|
83
|
+
token_address = token_address.strip()
|
|
84
|
+
if not chain:
|
|
85
|
+
raise ProviderError("dexscreener", "chain must not be empty.")
|
|
86
|
+
if not token_address:
|
|
87
|
+
raise ProviderError("dexscreener", "token_address must not be empty.")
|
|
88
|
+
cache_key = f"token:{chain}:{token_address.lower()}"
|
|
89
|
+
cached = _cache_get(cache_key)
|
|
90
|
+
if cached is not None:
|
|
91
|
+
return cached
|
|
92
|
+
data = await _get(f"/tokens/v1/{chain}/{token_address}")
|
|
93
|
+
pairs = _extract_pairs(data)
|
|
94
|
+
_cache_set(cache_key, pairs)
|
|
95
|
+
return pairs
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def normalize_pair(pair: dict[str, Any]) -> dict[str, Any]:
|
|
99
|
+
"""Flatten a DexScreener pair payload into the shape handed back to the agent."""
|
|
100
|
+
base_token = pair.get("baseToken") or {}
|
|
101
|
+
quote_token = pair.get("quoteToken") or {}
|
|
102
|
+
volume = pair.get("volume") or {}
|
|
103
|
+
liquidity = pair.get("liquidity") or {}
|
|
104
|
+
price_change = pair.get("priceChange") or {}
|
|
105
|
+
txns = pair.get("txns") or {}
|
|
106
|
+
return {
|
|
107
|
+
"chain_id": pair.get("chainId"),
|
|
108
|
+
"dex_id": pair.get("dexId"),
|
|
109
|
+
"pair_address": pair.get("pairAddress"),
|
|
110
|
+
"url": pair.get("url"),
|
|
111
|
+
"labels": pair.get("labels") or [],
|
|
112
|
+
"base_token": {
|
|
113
|
+
"address": base_token.get("address"),
|
|
114
|
+
"name": base_token.get("name"),
|
|
115
|
+
"symbol": base_token.get("symbol"),
|
|
116
|
+
},
|
|
117
|
+
"quote_token": {
|
|
118
|
+
"address": quote_token.get("address"),
|
|
119
|
+
"name": quote_token.get("name"),
|
|
120
|
+
"symbol": quote_token.get("symbol"),
|
|
121
|
+
},
|
|
122
|
+
"price_usd": pair.get("priceUsd"),
|
|
123
|
+
"price_native": pair.get("priceNative"),
|
|
124
|
+
"price_change_pct": {
|
|
125
|
+
"m5": price_change.get("m5"),
|
|
126
|
+
"h1": price_change.get("h1"),
|
|
127
|
+
"h6": price_change.get("h6"),
|
|
128
|
+
"h24": price_change.get("h24"),
|
|
129
|
+
},
|
|
130
|
+
"volume_usd": {
|
|
131
|
+
"m5": volume.get("m5"),
|
|
132
|
+
"h1": volume.get("h1"),
|
|
133
|
+
"h6": volume.get("h6"),
|
|
134
|
+
"h24": volume.get("h24"),
|
|
135
|
+
},
|
|
136
|
+
"txns": {
|
|
137
|
+
window: {"buys": counts.get("buys"), "sells": counts.get("sells")}
|
|
138
|
+
for window, counts in txns.items()
|
|
139
|
+
if isinstance(counts, dict)
|
|
140
|
+
},
|
|
141
|
+
"liquidity_usd": liquidity.get("usd"),
|
|
142
|
+
"fdv_usd": pair.get("fdv"),
|
|
143
|
+
"market_cap_usd": pair.get("marketCap"),
|
|
144
|
+
"pair_created_at": pair.get("pairCreatedAt"),
|
|
145
|
+
}
|
|
@@ -325,6 +325,18 @@ class AgentWalletBackend(ABC):
|
|
|
325
325
|
) -> dict[str, Any]:
|
|
326
326
|
raise WalletBackendError(f"{self.name} does not support Uniswap swap previews.")
|
|
327
327
|
|
|
328
|
+
async def search_uniswap_pairs(
|
|
329
|
+
self,
|
|
330
|
+
*,
|
|
331
|
+
query: str | None = None,
|
|
332
|
+
token_address: str | None = None,
|
|
333
|
+
chain: str | None = None,
|
|
334
|
+
dex_id: str | None = None,
|
|
335
|
+
all_chains: bool = False,
|
|
336
|
+
limit: int = 10,
|
|
337
|
+
) -> dict[str, Any]:
|
|
338
|
+
raise WalletBackendError(f"{self.name} does not support Uniswap pair search.")
|
|
339
|
+
|
|
328
340
|
async def send_uniswap_swap(
|
|
329
341
|
self,
|
|
330
342
|
*,
|
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from agent_wallet.config import normalize_evm_network
|
|
9
9
|
from agent_wallet.providers.evm_portfolio import build_portfolio_snapshot
|
|
10
|
-
from agent_wallet.providers import lifi
|
|
10
|
+
from agent_wallet.providers import dexscreener, lifi
|
|
11
11
|
from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient
|
|
12
12
|
from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendError, WalletCapabilities
|
|
13
13
|
|
|
@@ -1920,6 +1920,43 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1920
1920
|
"swap_transaction": swap_transaction,
|
|
1921
1921
|
}
|
|
1922
1922
|
|
|
1923
|
+
async def search_uniswap_pairs(
|
|
1924
|
+
self,
|
|
1925
|
+
*,
|
|
1926
|
+
query: str | None = None,
|
|
1927
|
+
token_address: str | None = None,
|
|
1928
|
+
chain: str | None = None,
|
|
1929
|
+
dex_id: str | None = None,
|
|
1930
|
+
all_chains: bool = False,
|
|
1931
|
+
limit: int = 10,
|
|
1932
|
+
) -> dict[str, Any]:
|
|
1933
|
+
query = query.strip() if isinstance(query, str) else None
|
|
1934
|
+
token_address = token_address.strip() if isinstance(token_address, str) else None
|
|
1935
|
+
if not query and not token_address:
|
|
1936
|
+
raise WalletBackendError("Either query or token_address is required.")
|
|
1937
|
+
resolved_chain = (chain or self.network).strip().lower()
|
|
1938
|
+
if token_address:
|
|
1939
|
+
pairs = await dexscreener.get_pairs_for_token(chain=resolved_chain, token_address=token_address)
|
|
1940
|
+
else:
|
|
1941
|
+
pairs = await dexscreener.search_pairs(query)
|
|
1942
|
+
if not all_chains:
|
|
1943
|
+
pairs = [p for p in pairs if str(p.get("chainId") or "").lower() == resolved_chain]
|
|
1944
|
+
if dex_id:
|
|
1945
|
+
wanted_dex = dex_id.strip().lower()
|
|
1946
|
+
pairs = [p for p in pairs if str(p.get("dexId") or "").lower() == wanted_dex]
|
|
1947
|
+
bounded_limit = max(1, min(int(limit or 10), 30))
|
|
1948
|
+
normalized = [dexscreener.normalize_pair(pair) for pair in pairs[:bounded_limit]]
|
|
1949
|
+
return {
|
|
1950
|
+
"chain": None if all_chains else resolved_chain,
|
|
1951
|
+
"all_chains": all_chains,
|
|
1952
|
+
"query": query,
|
|
1953
|
+
"token_address": token_address,
|
|
1954
|
+
"dex_id": dex_id,
|
|
1955
|
+
"count": len(normalized),
|
|
1956
|
+
"pairs": normalized,
|
|
1957
|
+
"source": "dexscreener",
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1923
1960
|
async def send_uniswap_swap(
|
|
1924
1961
|
self,
|
|
1925
1962
|
*,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.89",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-wallet",
|
|
3
3
|
"displayName": "Agent Wallet",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.89",
|
|
5
5
|
"description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AgentLayer"
|
package/package.json
CHANGED