@agentlayer.tech/wallet 0.1.84 → 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/CHANGELOG.md +15 -0
- 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 +46 -9
- 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/.env.example +3 -1
- package/wdk-evm-wallet/README.md +1 -0
- package/wdk-evm-wallet/package.json +1 -1
- package/wdk-evm-wallet/src/config.js +8 -2
- package/wdk-evm-wallet/src/server.js +18 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +316 -34
|
@@ -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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Hardened Morpho EVM operations with a 30% gas-limit buffer calculated from
|
|
6
|
+
the exact final calldata immediately before sending. Morpho vault and market
|
|
7
|
+
writes now wait for their final on-chain receipt and report reverts instead
|
|
8
|
+
of returning an unconfirmed broadcast as a successful operation.
|
|
9
|
+
|
|
10
|
+
- Extended the same exact-calldata 30% gas-limit buffer and receipt validation
|
|
11
|
+
to Aave, Lido, Velora, LI.FI, and locally broadcast Uniswap swaps. This
|
|
12
|
+
includes Robinhood Chain's Universal Router, direct V3 fallback, and native
|
|
13
|
+
wrap/unwrap paths; UniswapX orders remain order submissions, not local
|
|
14
|
+
on-chain confirmations.
|
|
15
|
+
|
|
16
|
+
- Fixed the Robinhood Chain Uniswap default to Universal Router `2.1.1`.
|
|
17
|
+
`UNISWAP_ROUTER_VERSION_BY_NETWORK` can still explicitly override it per
|
|
18
|
+
network; the legacy global `2.0` default continues to serve Ethereum and Base.
|
|
19
|
+
|
|
5
20
|
## v0.1.77 - 2026-07-13
|
|
6
21
|
|
|
7
22
|
- Added Robinhood Chain mainnet (`chainId` 4663) to the local EVM wallet,
|
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
|
|
|
@@ -1240,7 +1240,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1240
1240
|
sign_only=self.sign_only,
|
|
1241
1241
|
),
|
|
1242
1242
|
"broadcasted": True,
|
|
1243
|
-
"confirmed":
|
|
1243
|
+
"confirmed": bool(data.get("confirmed")),
|
|
1244
1244
|
}
|
|
1245
1245
|
|
|
1246
1246
|
async def preview_evm_lido_operation(
|
|
@@ -1308,7 +1308,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1308
1308
|
sign_only=self.sign_only,
|
|
1309
1309
|
),
|
|
1310
1310
|
"broadcasted": True,
|
|
1311
|
-
"confirmed":
|
|
1311
|
+
"confirmed": bool(data.get("confirmed")),
|
|
1312
1312
|
}
|
|
1313
1313
|
|
|
1314
1314
|
async def preview_evm_lido_withdrawal(
|
|
@@ -1385,7 +1385,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1385
1385
|
sign_only=self.sign_only,
|
|
1386
1386
|
),
|
|
1387
1387
|
"broadcasted": True,
|
|
1388
|
-
"confirmed":
|
|
1388
|
+
"confirmed": bool(data.get("confirmed")),
|
|
1389
1389
|
}
|
|
1390
1390
|
|
|
1391
1391
|
async def preview_evm_morpho_vault_operation(
|
|
@@ -1480,7 +1480,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1480
1480
|
sign_only=self.sign_only,
|
|
1481
1481
|
),
|
|
1482
1482
|
"broadcasted": True,
|
|
1483
|
-
"confirmed":
|
|
1483
|
+
"confirmed": bool(data.get("confirmed")),
|
|
1484
1484
|
}
|
|
1485
1485
|
|
|
1486
1486
|
async def preview_evm_morpho_market_operation(
|
|
@@ -1575,7 +1575,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1575
1575
|
sign_only=self.sign_only,
|
|
1576
1576
|
),
|
|
1577
1577
|
"broadcasted": True,
|
|
1578
|
-
"confirmed":
|
|
1578
|
+
"confirmed": bool(data.get("confirmed")),
|
|
1579
1579
|
}
|
|
1580
1580
|
|
|
1581
1581
|
async def get_evm_swap_quote(
|
|
@@ -1816,7 +1816,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1816
1816
|
"result": result,
|
|
1817
1817
|
"chain_id": int(data.get("chainId") or 0),
|
|
1818
1818
|
"broadcasted": True,
|
|
1819
|
-
"confirmed":
|
|
1819
|
+
"confirmed": bool(data.get("confirmed")),
|
|
1820
1820
|
"source": "wdk-evm-wallet",
|
|
1821
1821
|
}
|
|
1822
1822
|
|
|
@@ -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
|
*,
|
|
@@ -1987,7 +2024,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1987
2024
|
"chain_id": int(data.get("chainId") or 0),
|
|
1988
2025
|
"broadcasted": bool(result.get("hash")),
|
|
1989
2026
|
"order_submitted": order_id is not None,
|
|
1990
|
-
"confirmed":
|
|
2027
|
+
"confirmed": bool(data.get("confirmed")),
|
|
1991
2028
|
"source": str(data.get("source") or "wdk-evm-wallet"),
|
|
1992
2029
|
}
|
|
1993
2030
|
|
|
@@ -2102,7 +2139,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
2102
2139
|
"reset_allowance_hash": result.get("resetAllowanceHash"),
|
|
2103
2140
|
"result": result,
|
|
2104
2141
|
"broadcasted": True,
|
|
2105
|
-
"confirmed":
|
|
2142
|
+
"confirmed": bool(data.get("confirmed")),
|
|
2106
2143
|
}
|
|
2107
2144
|
|
|
2108
2145
|
async def preview_evm_native_transfer(
|
|
@@ -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
|
@@ -29,5 +29,7 @@ UNISWAP_API_KEY=
|
|
|
29
29
|
UNISWAP_ROUTER_VERSION=2.0
|
|
30
30
|
# Optional per-network override; each value must exist in the wallet's reviewed
|
|
31
31
|
# execution profile and match the provider-gateway's per-chain configuration.
|
|
32
|
-
|
|
32
|
+
# Robinhood Chain has no Universal Router 2.0 deployment (Uniswap defaults it
|
|
33
|
+
# to 2.1.1); requesting 2.0 makes the Trading API 404 every pair on that chain.
|
|
34
|
+
UNISWAP_ROUTER_VERSION_BY_NETWORK={"ethereum":"2.0","base":"2.0","robinhood":"2.1.1"}
|
|
33
35
|
UNISWAP_DEFAULT_SLIPPAGE_BPS=300
|
package/wdk-evm-wallet/README.md
CHANGED
|
@@ -57,6 +57,12 @@ const DEFAULT_NETWORK_PROFILES = {
|
|
|
57
57
|
},
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// Robinhood Chain uses the Universal Router 2.1.1 deployment. Keep this
|
|
61
|
+
// network-specific default separate from the legacy global 2.0 setting so
|
|
62
|
+
// Ethereum and Base retain their existing routers while Robinhood can quote
|
|
63
|
+
// and execute without a local environment override.
|
|
64
|
+
const DEFAULT_UNISWAP_ROUTER_VERSIONS_BY_NETWORK = { robinhood: "2.1.1" };
|
|
65
|
+
|
|
60
66
|
const SUPPORTED_GATEWAY_PROVIDERS = new Set(["auto", "shared", "alchemy"]);
|
|
61
67
|
|
|
62
68
|
function parseProviderMode(value, fallback = "public") {
|
|
@@ -192,7 +198,7 @@ function normalizeNetworkKey(value) {
|
|
|
192
198
|
function parseUniswapRouterVersionsByNetwork(value) {
|
|
193
199
|
const raw = String(value ?? "").trim();
|
|
194
200
|
if (!raw) {
|
|
195
|
-
return {};
|
|
201
|
+
return { ...DEFAULT_UNISWAP_ROUTER_VERSIONS_BY_NETWORK };
|
|
196
202
|
}
|
|
197
203
|
let parsed;
|
|
198
204
|
try {
|
|
@@ -215,7 +221,7 @@ function parseUniswapRouterVersionsByNetwork(value) {
|
|
|
215
221
|
}
|
|
216
222
|
normalized[networkKey] = normalizedVersion;
|
|
217
223
|
}
|
|
218
|
-
return normalized;
|
|
224
|
+
return { ...DEFAULT_UNISWAP_ROUTER_VERSIONS_BY_NETWORK, ...normalized };
|
|
219
225
|
}
|
|
220
226
|
|
|
221
227
|
function joinUrl(base, pathname) {
|
|
@@ -37,12 +37,21 @@ function normalizeErrorCode(errorCode, pathname, message) {
|
|
|
37
37
|
code === "aave_quote_changed" ||
|
|
38
38
|
code === "aave_approval_required" ||
|
|
39
39
|
code === "aave_fee_unavailable" ||
|
|
40
|
+
code === "aave_operation_reverted" ||
|
|
41
|
+
code === "aave_operation_confirmation_timeout" ||
|
|
40
42
|
code === "aave_cleanup_failed" ||
|
|
41
43
|
code === "morpho_api_failed" ||
|
|
42
44
|
code === "morpho_quote_changed" ||
|
|
43
45
|
code === "morpho_requirements_unresolved" ||
|
|
44
46
|
code === "morpho_fee_unavailable" ||
|
|
45
47
|
code === "morpho_cleanup_failed" ||
|
|
48
|
+
code === "defi_gas_estimate_unavailable" ||
|
|
49
|
+
code === "lido_operation_reverted" ||
|
|
50
|
+
code === "lido_operation_confirmation_timeout" ||
|
|
51
|
+
code === "lido_withdrawal_reverted" ||
|
|
52
|
+
code === "lido_withdrawal_confirmation_timeout" ||
|
|
53
|
+
code === "swap_reverted" ||
|
|
54
|
+
code === "swap_confirmation_timeout" ||
|
|
46
55
|
code === "token_transfer_failed" ||
|
|
47
56
|
code === "fee_limit_exceeded" ||
|
|
48
57
|
code === "token_read_failed" ||
|
|
@@ -138,11 +147,20 @@ function errorStatusCode(errorCode, fallback = 400) {
|
|
|
138
147
|
errorCode === "swap_cleanup_failed" ||
|
|
139
148
|
errorCode === "aave_approval_required" ||
|
|
140
149
|
errorCode === "aave_fee_unavailable" ||
|
|
150
|
+
errorCode === "aave_operation_reverted" ||
|
|
151
|
+
errorCode === "aave_operation_confirmation_timeout" ||
|
|
141
152
|
errorCode === "aave_cleanup_failed" ||
|
|
142
153
|
errorCode === "morpho_api_failed" ||
|
|
143
154
|
errorCode === "morpho_requirements_unresolved" ||
|
|
144
155
|
errorCode === "morpho_fee_unavailable" ||
|
|
145
156
|
errorCode === "morpho_cleanup_failed" ||
|
|
157
|
+
errorCode === "defi_gas_estimate_unavailable" ||
|
|
158
|
+
errorCode === "lido_operation_reverted" ||
|
|
159
|
+
errorCode === "lido_operation_confirmation_timeout" ||
|
|
160
|
+
errorCode === "lido_withdrawal_reverted" ||
|
|
161
|
+
errorCode === "lido_withdrawal_confirmation_timeout" ||
|
|
162
|
+
errorCode === "swap_reverted" ||
|
|
163
|
+
errorCode === "swap_confirmation_timeout" ||
|
|
146
164
|
errorCode === "token_transfer_failed" ||
|
|
147
165
|
errorCode === "fee_limit_exceeded" ||
|
|
148
166
|
errorCode === "uniswap_api_key_missing"
|
|
@@ -41,12 +41,17 @@ const UNISWAP_EXECUTION_PROFILES = {
|
|
|
41
41
|
},
|
|
42
42
|
robinhood: {
|
|
43
43
|
chainId: 4663,
|
|
44
|
-
|
|
44
|
+
// Robinhood Chain has no Universal Router 2.0 deployment - Uniswap
|
|
45
|
+
// defaults it (like Ink) to 2.1.1, and requesting "2.0" makes the Trading
|
|
46
|
+
// API return a blanket "No quotes available" for every pair on this
|
|
47
|
+
// chain. See https://blog.uniswap.org/robinhood-chain-is-live. Same
|
|
48
|
+
// contract address as before; only the version label changes.
|
|
49
|
+
universalRouters: { "2.1.1": "0x8876789976decbfcbbbe364623c63652db8c0904" },
|
|
45
50
|
wrappedNative: "0x0bd7d308f8e1639fab988df18a8011f41eacad73",
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
ammProtocols: ["V3"],
|
|
51
|
+
// Full protocol stack like ethereum/base - the earlier V3-only filter
|
|
52
|
+
// wasn't the actual blocker (see router version note above) but there's
|
|
53
|
+
// no reason to hide V2/V4 routes from the Trading API either.
|
|
54
|
+
ammProtocols: ["V2", "V3", "V4"],
|
|
50
55
|
v3DirectFallback: {
|
|
51
56
|
quoterV2: "0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7",
|
|
52
57
|
swapRouter02: "0xcaf681a66d020601342297493863e78c959e5cb2",
|
|
@@ -155,8 +160,20 @@ const MORPHO_MAX_LIST_LIMIT = 500;
|
|
|
155
160
|
// two minutes is below the indexer's own aggregation noise.
|
|
156
161
|
const MORPHO_DISCOVERY_CACHE_TTL_MS = 120_000;
|
|
157
162
|
const MORPHO_DISCOVERY_CACHE_MAX_ENTRIES = 64;
|
|
163
|
+
// DeFi routes can take a different on-chain branch between estimate time and
|
|
164
|
+
// inclusion. Estimate exact final calldata immediately before signing and use
|
|
165
|
+
// an explicit headroom rather than relying on the provider/WDK default.
|
|
166
|
+
const DEFI_GAS_BUFFER_BPS = 13_000n;
|
|
167
|
+
const MORPHO_GAS_BUFFER_BPS = DEFI_GAS_BUFFER_BPS;
|
|
168
|
+
const BPS_DENOMINATOR = 10_000n;
|
|
158
169
|
const morphoDiscoveryCache = new Map();
|
|
159
170
|
|
|
171
|
+
function applyGasBuffer(value, bufferBps = MORPHO_GAS_BUFFER_BPS) {
|
|
172
|
+
const normalizedValue = BigInt(value);
|
|
173
|
+
const normalizedBufferBps = BigInt(bufferBps);
|
|
174
|
+
return (normalizedValue * normalizedBufferBps + BPS_DENOMINATOR - 1n) / BPS_DENOMINATOR;
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
function morphoDiscoveryCacheGet(key) {
|
|
161
178
|
const entry = morphoDiscoveryCache.get(key);
|
|
162
179
|
if (!entry) {
|
|
@@ -2512,12 +2529,20 @@ export class WdkEvmWalletService {
|
|
|
2512
2529
|
const protocol = this.#createMorphoProtocol(account, runtimeConfig, request);
|
|
2513
2530
|
let result;
|
|
2514
2531
|
try {
|
|
2515
|
-
result = await
|
|
2516
|
-
|
|
2517
|
-
|
|
2532
|
+
result = await this.#sendMorphoProtocolOperation({
|
|
2533
|
+
account,
|
|
2534
|
+
runtimeConfig,
|
|
2535
|
+
protocol,
|
|
2536
|
+
request,
|
|
2537
|
+
});
|
|
2518
2538
|
} finally {
|
|
2519
2539
|
await maybeDispose(protocol);
|
|
2520
2540
|
}
|
|
2541
|
+
await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
|
|
2542
|
+
operationLabel: "Morpho operation",
|
|
2543
|
+
failureCode: "morpho_operation_reverted",
|
|
2544
|
+
timeoutCode: "morpho_operation_confirmation_timeout",
|
|
2545
|
+
});
|
|
2521
2546
|
const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
|
|
2522
2547
|
const totalFee = requirementExecution.totalFee + resultFee;
|
|
2523
2548
|
return {
|
|
@@ -2543,6 +2568,7 @@ export class WdkEvmWalletService {
|
|
|
2543
2568
|
requirementsFee: requirementExecution.totalFee.toString(),
|
|
2544
2569
|
requirements: requirementExecution.transactions,
|
|
2545
2570
|
},
|
|
2571
|
+
confirmed: true,
|
|
2546
2572
|
};
|
|
2547
2573
|
} catch (error) {
|
|
2548
2574
|
const cleanup = await this.#restoreMorphoRequirementsAfterFailedOperation({
|
|
@@ -2689,12 +2715,20 @@ export class WdkEvmWalletService {
|
|
|
2689
2715
|
const protocol = this.#createMorphoProtocol(account, runtimeConfig, request);
|
|
2690
2716
|
let result;
|
|
2691
2717
|
try {
|
|
2692
|
-
result = await
|
|
2693
|
-
|
|
2694
|
-
|
|
2718
|
+
result = await this.#sendMorphoProtocolOperation({
|
|
2719
|
+
account,
|
|
2720
|
+
runtimeConfig,
|
|
2721
|
+
protocol,
|
|
2722
|
+
request,
|
|
2723
|
+
});
|
|
2695
2724
|
} finally {
|
|
2696
2725
|
await maybeDispose(protocol);
|
|
2697
2726
|
}
|
|
2727
|
+
await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
|
|
2728
|
+
operationLabel: "Morpho operation",
|
|
2729
|
+
failureCode: "morpho_operation_reverted",
|
|
2730
|
+
timeoutCode: "morpho_operation_confirmation_timeout",
|
|
2731
|
+
});
|
|
2698
2732
|
const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
|
|
2699
2733
|
const totalFee = requirementExecution.totalFee + resultFee;
|
|
2700
2734
|
return {
|
|
@@ -2720,6 +2754,7 @@ export class WdkEvmWalletService {
|
|
|
2720
2754
|
requirementsFee: requirementExecution.totalFee.toString(),
|
|
2721
2755
|
requirements: requirementExecution.transactions,
|
|
2722
2756
|
},
|
|
2757
|
+
confirmed: true,
|
|
2723
2758
|
};
|
|
2724
2759
|
} catch (error) {
|
|
2725
2760
|
const cleanup = await this.#restoreMorphoRequirementsAfterFailedOperation({
|
|
@@ -2856,13 +2891,25 @@ export class WdkEvmWalletService {
|
|
|
2856
2891
|
const protocol = new AaveProtocolEvm(account);
|
|
2857
2892
|
let result;
|
|
2858
2893
|
try {
|
|
2859
|
-
result = await
|
|
2860
|
-
|
|
2861
|
-
|
|
2894
|
+
result = await this.#sendBufferedDefiProtocolOperation({
|
|
2895
|
+
account,
|
|
2896
|
+
runtimeConfig,
|
|
2897
|
+
operationLabel: `Aave ${request.operation}`,
|
|
2898
|
+
operation: request.operation,
|
|
2899
|
+
invoke: () =>
|
|
2900
|
+
protocol[request.operation]({
|
|
2901
|
+
token: request.token,
|
|
2902
|
+
amount: request.amount,
|
|
2903
|
+
}),
|
|
2862
2904
|
});
|
|
2863
2905
|
} finally {
|
|
2864
2906
|
await maybeDispose(protocol);
|
|
2865
2907
|
}
|
|
2908
|
+
await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
|
|
2909
|
+
operationLabel: "Aave operation",
|
|
2910
|
+
failureCode: "aave_operation_reverted",
|
|
2911
|
+
timeoutCode: "aave_operation_confirmation_timeout",
|
|
2912
|
+
});
|
|
2866
2913
|
const resultFee = BigInt(result?.fee || 0);
|
|
2867
2914
|
const totalFee = approvalExecution.totalFee + resultFee;
|
|
2868
2915
|
return {
|
|
@@ -2891,6 +2938,7 @@ export class WdkEvmWalletService {
|
|
|
2891
2938
|
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
2892
2939
|
: {}),
|
|
2893
2940
|
},
|
|
2941
|
+
confirmed: true,
|
|
2894
2942
|
};
|
|
2895
2943
|
} catch (error) {
|
|
2896
2944
|
const cleanup = await this.#restoreAllowanceAfterFailedAaveOperation({
|
|
@@ -3172,7 +3220,19 @@ export class WdkEvmWalletService {
|
|
|
3172
3220
|
);
|
|
3173
3221
|
}
|
|
3174
3222
|
|
|
3175
|
-
const result = await
|
|
3223
|
+
const result = await this.#sendBufferedDefiTransaction({
|
|
3224
|
+
account,
|
|
3225
|
+
runtimeConfig,
|
|
3226
|
+
from: address,
|
|
3227
|
+
tx: finalPlan.operationTx,
|
|
3228
|
+
operationLabel: `Lido ${request.operation}`,
|
|
3229
|
+
operation: request.operation,
|
|
3230
|
+
});
|
|
3231
|
+
await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
|
|
3232
|
+
operationLabel: "Lido operation",
|
|
3233
|
+
failureCode: "lido_operation_reverted",
|
|
3234
|
+
timeoutCode: "lido_operation_confirmation_timeout",
|
|
3235
|
+
});
|
|
3176
3236
|
const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
|
|
3177
3237
|
const totalFee = approvalExecution.totalFee + resultFee;
|
|
3178
3238
|
return {
|
|
@@ -3201,6 +3261,7 @@ export class WdkEvmWalletService {
|
|
|
3201
3261
|
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
3202
3262
|
: {}),
|
|
3203
3263
|
},
|
|
3264
|
+
confirmed: true,
|
|
3204
3265
|
};
|
|
3205
3266
|
} catch (error) {
|
|
3206
3267
|
const cleanup = await this.#restoreAllowanceAfterFailedLidoOperation({
|
|
@@ -3323,7 +3384,19 @@ export class WdkEvmWalletService {
|
|
|
3323
3384
|
);
|
|
3324
3385
|
}
|
|
3325
3386
|
|
|
3326
|
-
const result = await
|
|
3387
|
+
const result = await this.#sendBufferedDefiTransaction({
|
|
3388
|
+
account,
|
|
3389
|
+
runtimeConfig,
|
|
3390
|
+
from: address,
|
|
3391
|
+
tx: finalPlan.operationTx,
|
|
3392
|
+
operationLabel: `Lido ${request.operation}`,
|
|
3393
|
+
operation: request.operation,
|
|
3394
|
+
});
|
|
3395
|
+
await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
|
|
3396
|
+
operationLabel: "Lido withdrawal",
|
|
3397
|
+
failureCode: "lido_withdrawal_reverted",
|
|
3398
|
+
timeoutCode: "lido_withdrawal_confirmation_timeout",
|
|
3399
|
+
});
|
|
3327
3400
|
const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
|
|
3328
3401
|
const totalFee = approvalExecution.totalFee + resultFee;
|
|
3329
3402
|
return {
|
|
@@ -3352,6 +3425,7 @@ export class WdkEvmWalletService {
|
|
|
3352
3425
|
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
3353
3426
|
: {}),
|
|
3354
3427
|
},
|
|
3428
|
+
confirmed: true,
|
|
3355
3429
|
};
|
|
3356
3430
|
} catch (error) {
|
|
3357
3431
|
const cleanup = await this.#restoreAllowanceAfterFailedLidoWithdrawal({
|
|
@@ -3584,12 +3658,23 @@ export class WdkEvmWalletService {
|
|
|
3584
3658
|
})
|
|
3585
3659
|
: finalPlan.simulation;
|
|
3586
3660
|
this.#assertSimulationSucceeded(effectiveSimulation);
|
|
3587
|
-
const
|
|
3588
|
-
|
|
3661
|
+
const swapResult = await this.#sendBufferedDefiTransaction({
|
|
3662
|
+
account,
|
|
3663
|
+
runtimeConfig,
|
|
3664
|
+
from: address,
|
|
3665
|
+
tx: finalPlan.swapTx,
|
|
3666
|
+
operationLabel: "Velora swap",
|
|
3667
|
+
});
|
|
3668
|
+
await this.#waitForTransactionReceipt(runtimeConfig, swapResult.hash, {
|
|
3669
|
+
operationLabel: "Velora swap",
|
|
3670
|
+
failureCode: "swap_reverted",
|
|
3671
|
+
timeoutCode: "swap_confirmation_timeout",
|
|
3672
|
+
});
|
|
3673
|
+
const totalFee = approvalExecution.totalFee + BigInt(swapResult.fee);
|
|
3589
3674
|
const result = {
|
|
3590
|
-
|
|
3675
|
+
...swapResult,
|
|
3591
3676
|
fee: totalFee.toString(),
|
|
3592
|
-
swapFee:
|
|
3677
|
+
swapFee: swapResult.fee,
|
|
3593
3678
|
approvalFee: approvalExecution.totalFee.toString(),
|
|
3594
3679
|
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
3595
3680
|
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
@@ -3630,6 +3715,7 @@ export class WdkEvmWalletService {
|
|
|
3630
3715
|
simulation: effectiveSimulation,
|
|
3631
3716
|
swapTransaction: finalPlan.swapTransaction,
|
|
3632
3717
|
result,
|
|
3718
|
+
confirmed: true,
|
|
3633
3719
|
source: "wdk-protocol-swap-velora-evm",
|
|
3634
3720
|
};
|
|
3635
3721
|
} catch (error) {
|
|
@@ -3740,12 +3826,23 @@ export class WdkEvmWalletService {
|
|
|
3740
3826
|
: finalPlan.simulation;
|
|
3741
3827
|
this.#assertSimulationSucceeded(effectiveSimulation);
|
|
3742
3828
|
|
|
3743
|
-
const
|
|
3744
|
-
|
|
3829
|
+
const swapResult = await this.#sendBufferedDefiTransaction({
|
|
3830
|
+
account,
|
|
3831
|
+
runtimeConfig,
|
|
3832
|
+
from: sourceAddress,
|
|
3833
|
+
tx: finalPlan.swapTx,
|
|
3834
|
+
operationLabel: "LI.FI swap",
|
|
3835
|
+
});
|
|
3836
|
+
await this.#waitForTransactionReceipt(runtimeConfig, swapResult.hash, {
|
|
3837
|
+
operationLabel: "LI.FI swap",
|
|
3838
|
+
failureCode: "swap_reverted",
|
|
3839
|
+
timeoutCode: "swap_confirmation_timeout",
|
|
3840
|
+
});
|
|
3841
|
+
const totalFee = approvalExecution.totalFee + BigInt(swapResult.fee);
|
|
3745
3842
|
const result = {
|
|
3746
|
-
|
|
3843
|
+
...swapResult,
|
|
3747
3844
|
fee: totalFee.toString(),
|
|
3748
|
-
swapFee:
|
|
3845
|
+
swapFee: swapResult.fee,
|
|
3749
3846
|
approvalFee: approvalExecution.totalFee.toString(),
|
|
3750
3847
|
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
3751
3848
|
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
@@ -3772,6 +3869,7 @@ export class WdkEvmWalletService {
|
|
|
3772
3869
|
},
|
|
3773
3870
|
}),
|
|
3774
3871
|
result,
|
|
3872
|
+
confirmed: true,
|
|
3775
3873
|
};
|
|
3776
3874
|
} catch (error) {
|
|
3777
3875
|
const cleanup = await this.#restoreAllowanceAfterFailedSwap({
|
|
@@ -3967,14 +4065,25 @@ export class WdkEvmWalletService {
|
|
|
3967
4065
|
});
|
|
3968
4066
|
this.#assertSimulationSucceeded(simulation);
|
|
3969
4067
|
|
|
3970
|
-
const
|
|
4068
|
+
const swapResult = await this.#sendBufferedDefiTransaction({
|
|
4069
|
+
account,
|
|
4070
|
+
runtimeConfig,
|
|
4071
|
+
from: address,
|
|
4072
|
+
tx: swapTx,
|
|
4073
|
+
operationLabel: "Uniswap swap",
|
|
4074
|
+
});
|
|
4075
|
+
await this.#waitForTransactionReceipt(runtimeConfig, swapResult.hash, {
|
|
4076
|
+
operationLabel: "Uniswap swap",
|
|
4077
|
+
failureCode: "swap_reverted",
|
|
4078
|
+
timeoutCode: "swap_confirmation_timeout",
|
|
4079
|
+
});
|
|
3971
4080
|
swapTransaction = {
|
|
3972
4081
|
to: swapTx.to,
|
|
3973
4082
|
value: swapTx.value.toString(),
|
|
3974
4083
|
dataHash: sha256Hex(swapTx.data),
|
|
3975
4084
|
};
|
|
3976
4085
|
result = {
|
|
3977
|
-
|
|
4086
|
+
...swapResult,
|
|
3978
4087
|
approvalFee: approvalExecution.totalFee.toString(),
|
|
3979
4088
|
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
3980
4089
|
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
@@ -4001,6 +4110,7 @@ export class WdkEvmWalletService {
|
|
|
4001
4110
|
simulation,
|
|
4002
4111
|
swapTransaction,
|
|
4003
4112
|
result,
|
|
4113
|
+
confirmed: Boolean(result.hash),
|
|
4004
4114
|
};
|
|
4005
4115
|
} catch (error) {
|
|
4006
4116
|
const cleanup = await this.#restoreAllowanceAfterFailedSwap({
|
|
@@ -4536,6 +4646,150 @@ export class WdkEvmWalletService {
|
|
|
4536
4646
|
return options;
|
|
4537
4647
|
}
|
|
4538
4648
|
|
|
4649
|
+
async #sendMorphoProtocolOperation({ account, runtimeConfig, protocol, request }) {
|
|
4650
|
+
const methods = this.#getMorphoOperationMethods(request);
|
|
4651
|
+
const originalSendTransaction = account.sendTransaction;
|
|
4652
|
+
if (typeof originalSendTransaction !== "function") {
|
|
4653
|
+
throw new Error("Morpho operation requires an EVM account with sendTransaction().");
|
|
4654
|
+
}
|
|
4655
|
+
const hadOwnSendTransaction = Object.prototype.hasOwnProperty.call(account, "sendTransaction");
|
|
4656
|
+
|
|
4657
|
+
// The protocol constructs the final calldata immediately before sending it.
|
|
4658
|
+
// Intercept that one protocol-scoped send so the estimate and padded limit
|
|
4659
|
+
// are based on the exact transaction, rather than a stale preview.
|
|
4660
|
+
account.sendTransaction = async (tx) => {
|
|
4661
|
+
const gas = await this.#prepareMorphoGasLimit({
|
|
4662
|
+
runtimeConfig,
|
|
4663
|
+
from: await account.getAddress(),
|
|
4664
|
+
tx,
|
|
4665
|
+
operation: request.operation,
|
|
4666
|
+
});
|
|
4667
|
+
const result = await originalSendTransaction.call(account, {
|
|
4668
|
+
...tx,
|
|
4669
|
+
gasLimit: gas.gasLimit,
|
|
4670
|
+
});
|
|
4671
|
+
return {
|
|
4672
|
+
...result,
|
|
4673
|
+
fee: gas.maximumFee,
|
|
4674
|
+
gasEstimate: gas.gasEstimate.toString(),
|
|
4675
|
+
gasLimit: gas.gasLimit.toString(),
|
|
4676
|
+
gasBufferBps: MORPHO_GAS_BUFFER_BPS.toString(),
|
|
4677
|
+
};
|
|
4678
|
+
};
|
|
4679
|
+
|
|
4680
|
+
try {
|
|
4681
|
+
return await protocol[methods.sendMethod](this.#buildMorphoOperationOptions(request));
|
|
4682
|
+
} finally {
|
|
4683
|
+
if (hadOwnSendTransaction) {
|
|
4684
|
+
account.sendTransaction = originalSendTransaction;
|
|
4685
|
+
} else {
|
|
4686
|
+
delete account.sendTransaction;
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
|
|
4691
|
+
async #prepareMorphoGasLimit({ runtimeConfig, from, tx, operation }) {
|
|
4692
|
+
return this.#prepareBufferedDefiGasLimit({
|
|
4693
|
+
runtimeConfig,
|
|
4694
|
+
from,
|
|
4695
|
+
tx,
|
|
4696
|
+
operationLabel: `morpho ${operation}`,
|
|
4697
|
+
unavailableCode: "morpho_gas_estimate_unavailable",
|
|
4698
|
+
unavailableMessage: "Morpho gas estimate was empty. Generate a new quote before sending.",
|
|
4699
|
+
});
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4702
|
+
async #prepareBufferedDefiGasLimit({
|
|
4703
|
+
runtimeConfig,
|
|
4704
|
+
from,
|
|
4705
|
+
tx,
|
|
4706
|
+
operationLabel,
|
|
4707
|
+
unavailableCode = "defi_gas_estimate_unavailable",
|
|
4708
|
+
unavailableMessage = "DeFi gas estimate was empty. Generate a new quote before sending.",
|
|
4709
|
+
}) {
|
|
4710
|
+
const gasEstimateHex = await rpcRequest(runtimeConfig.providerUrl, "eth_estimateGas", [
|
|
4711
|
+
{
|
|
4712
|
+
from: normalizeAddress(from, "from"),
|
|
4713
|
+
to: normalizeAddress(String(tx.to || ""), "to"),
|
|
4714
|
+
data: assertNonEmptyString(String(tx.data || ""), "data"),
|
|
4715
|
+
value: toRpcHex(tx.value || 0),
|
|
4716
|
+
},
|
|
4717
|
+
]);
|
|
4718
|
+
const gasEstimate = BigInt(gasEstimateHex || "0x0");
|
|
4719
|
+
if (gasEstimate <= 0n) {
|
|
4720
|
+
throw createTaggedError(unavailableMessage, unavailableCode, { operation: operationLabel });
|
|
4721
|
+
}
|
|
4722
|
+
const gasLimit = applyGasBuffer(gasEstimate, DEFI_GAS_BUFFER_BPS);
|
|
4723
|
+
const effectiveFeePerGas = await this.#getEffectiveGasPrice(runtimeConfig);
|
|
4724
|
+
const maximumFee = gasLimit * effectiveFeePerGas;
|
|
4725
|
+
this.#assertMaxFee(runtimeConfig, maximumFee, operationLabel);
|
|
4726
|
+
return { gasEstimate, gasLimit, maximumFee };
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
async #sendBufferedDefiTransaction({ account, runtimeConfig, from, tx, operationLabel }) {
|
|
4730
|
+
return this.#sendBufferedDefiTransactionWithSender({
|
|
4731
|
+
sendTransaction: (preparedTx) => account.sendTransaction(preparedTx),
|
|
4732
|
+
runtimeConfig,
|
|
4733
|
+
from,
|
|
4734
|
+
tx,
|
|
4735
|
+
operationLabel,
|
|
4736
|
+
});
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
async #sendBufferedDefiTransactionWithSender({
|
|
4740
|
+
sendTransaction,
|
|
4741
|
+
runtimeConfig,
|
|
4742
|
+
from,
|
|
4743
|
+
tx,
|
|
4744
|
+
operationLabel,
|
|
4745
|
+
}) {
|
|
4746
|
+
const gas = await this.#prepareBufferedDefiGasLimit({
|
|
4747
|
+
runtimeConfig,
|
|
4748
|
+
from,
|
|
4749
|
+
tx,
|
|
4750
|
+
operationLabel,
|
|
4751
|
+
});
|
|
4752
|
+
const result = await sendTransaction({ ...tx, gasLimit: gas.gasLimit });
|
|
4753
|
+
return {
|
|
4754
|
+
...result,
|
|
4755
|
+
fee: gas.maximumFee.toString(),
|
|
4756
|
+
gasEstimate: gas.gasEstimate.toString(),
|
|
4757
|
+
gasLimit: gas.gasLimit.toString(),
|
|
4758
|
+
gasBufferBps: DEFI_GAS_BUFFER_BPS.toString(),
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
async #sendBufferedDefiProtocolOperation({
|
|
4763
|
+
account,
|
|
4764
|
+
runtimeConfig,
|
|
4765
|
+
operationLabel,
|
|
4766
|
+
operation,
|
|
4767
|
+
invoke,
|
|
4768
|
+
}) {
|
|
4769
|
+
const originalSendTransaction = account.sendTransaction;
|
|
4770
|
+
if (typeof originalSendTransaction !== "function") {
|
|
4771
|
+
throw new Error(`${operationLabel} requires an EVM account with sendTransaction().`);
|
|
4772
|
+
}
|
|
4773
|
+
const hadOwnSendTransaction = Object.prototype.hasOwnProperty.call(account, "sendTransaction");
|
|
4774
|
+
account.sendTransaction = async (tx) =>
|
|
4775
|
+
this.#sendBufferedDefiTransactionWithSender({
|
|
4776
|
+
sendTransaction: (preparedTx) => originalSendTransaction.call(account, preparedTx),
|
|
4777
|
+
runtimeConfig,
|
|
4778
|
+
from: await account.getAddress(),
|
|
4779
|
+
tx,
|
|
4780
|
+
operationLabel: `${operationLabel} ${operation}`,
|
|
4781
|
+
});
|
|
4782
|
+
try {
|
|
4783
|
+
return await invoke();
|
|
4784
|
+
} finally {
|
|
4785
|
+
if (hadOwnSendTransaction) {
|
|
4786
|
+
account.sendTransaction = originalSendTransaction;
|
|
4787
|
+
} else {
|
|
4788
|
+
delete account.sendTransaction;
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4539
4793
|
async #buildMorphoOperationPlan({
|
|
4540
4794
|
account,
|
|
4541
4795
|
runtimeConfig,
|
|
@@ -4736,7 +4990,7 @@ export class WdkEvmWalletService {
|
|
|
4736
4990
|
try {
|
|
4737
4991
|
const quote = await protocol[methods.quoteMethod](operationOptions);
|
|
4738
4992
|
return {
|
|
4739
|
-
fee: BigInt(quote?.fee || 0),
|
|
4993
|
+
fee: applyGasBuffer(BigInt(quote?.fee || 0), MORPHO_GAS_BUFFER_BPS),
|
|
4740
4994
|
error: null,
|
|
4741
4995
|
};
|
|
4742
4996
|
} catch (error) {
|
|
@@ -4833,11 +5087,23 @@ export class WdkEvmWalletService {
|
|
|
4833
5087
|
for (let index = 0; index < plan.requirements.transactions.length; index += 1) {
|
|
4834
5088
|
const requirementTx = plan.requirements.transactions[index];
|
|
4835
5089
|
const step = plan.requirements.steps[index];
|
|
4836
|
-
const
|
|
5090
|
+
const tx = {
|
|
4837
5091
|
to: requirementTx.to,
|
|
4838
5092
|
value: requirementTx.value ?? 0n,
|
|
4839
5093
|
data: requirementTx.data,
|
|
4840
|
-
}
|
|
5094
|
+
};
|
|
5095
|
+
// ERC-20 approval writes intentionally retain the narrow WDK path. A
|
|
5096
|
+
// Morpho authorization is a protocol write, so give it the same buffer
|
|
5097
|
+
// as the potentially stateful final operation.
|
|
5098
|
+
const result = step?.type === "authorization"
|
|
5099
|
+
? await this.#sendBufferedDefiTransaction({
|
|
5100
|
+
account,
|
|
5101
|
+
runtimeConfig,
|
|
5102
|
+
from: await account.getAddress(),
|
|
5103
|
+
tx,
|
|
5104
|
+
operationLabel: "Morpho authorization",
|
|
5105
|
+
})
|
|
5106
|
+
: await account.sendTransaction(tx);
|
|
4841
5107
|
const fee = BigInt(result?.fee || 0);
|
|
4842
5108
|
totalFee += fee;
|
|
4843
5109
|
transactions.push({
|
|
@@ -4913,13 +5179,19 @@ export class WdkEvmWalletService {
|
|
|
4913
5179
|
}
|
|
4914
5180
|
|
|
4915
5181
|
for (const context of requirementExecution.authorizationContexts || []) {
|
|
4916
|
-
const result = await
|
|
5182
|
+
const result = await this.#sendBufferedDefiTransaction({
|
|
5183
|
+
account,
|
|
5184
|
+
runtimeConfig,
|
|
5185
|
+
from: await account.getAddress(),
|
|
5186
|
+
operationLabel: "Morpho authorization cleanup",
|
|
5187
|
+
tx: {
|
|
4917
5188
|
to: context.contractAddress,
|
|
4918
5189
|
value: 0n,
|
|
4919
5190
|
data: MORPHO_AUTHORIZATION_INTERFACE.encodeFunctionData("setAuthorization", [
|
|
4920
5191
|
context.authorized,
|
|
4921
5192
|
false,
|
|
4922
5193
|
]),
|
|
5194
|
+
},
|
|
4923
5195
|
});
|
|
4924
5196
|
cleanup.authorizations.push({
|
|
4925
5197
|
contractAddress: context.contractAddress,
|
|
@@ -8030,13 +8302,21 @@ export class WdkEvmWalletService {
|
|
|
8030
8302
|
}
|
|
8031
8303
|
}
|
|
8032
8304
|
|
|
8033
|
-
async #waitForTransactionReceipt(
|
|
8305
|
+
async #waitForTransactionReceipt(
|
|
8306
|
+
runtimeConfig,
|
|
8307
|
+
txHash,
|
|
8308
|
+
{
|
|
8309
|
+
operationLabel = "Approval transaction",
|
|
8310
|
+
failureCode = "swap_approval_failed",
|
|
8311
|
+
timeoutCode = "swap_approval_timeout",
|
|
8312
|
+
} = {}
|
|
8313
|
+
) {
|
|
8034
8314
|
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
8035
8315
|
const receipt = await rpcRequest(runtimeConfig.providerUrl, "eth_getTransactionReceipt", [txHash]);
|
|
8036
8316
|
if (receipt) {
|
|
8037
8317
|
const status = String(receipt.status || "").toLowerCase();
|
|
8038
8318
|
if (status === "0x0") {
|
|
8039
|
-
throw createTaggedError(
|
|
8319
|
+
throw createTaggedError(`${operationLabel} reverted onchain.`, failureCode, {
|
|
8040
8320
|
txHash,
|
|
8041
8321
|
network: runtimeConfig.network,
|
|
8042
8322
|
});
|
|
@@ -8046,8 +8326,8 @@ export class WdkEvmWalletService {
|
|
|
8046
8326
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
8047
8327
|
}
|
|
8048
8328
|
throw createTaggedError(
|
|
8049
|
-
|
|
8050
|
-
|
|
8329
|
+
`Timed out waiting for ${operationLabel.toLowerCase()} confirmation.`,
|
|
8330
|
+
timeoutCode,
|
|
8051
8331
|
{
|
|
8052
8332
|
txHash,
|
|
8053
8333
|
network: runtimeConfig.network,
|
|
@@ -8057,6 +8337,8 @@ export class WdkEvmWalletService {
|
|
|
8057
8337
|
}
|
|
8058
8338
|
|
|
8059
8339
|
export const __testables = {
|
|
8340
|
+
applyGasBuffer,
|
|
8341
|
+
MORPHO_GAS_BUFFER_BPS,
|
|
8060
8342
|
PERMIT2_ADDRESS,
|
|
8061
8343
|
UNISWAP_SUPPORTED_CHAIN_IDS,
|
|
8062
8344
|
UNISWAP_EXECUTION_PROFILES,
|