@agentlayer.tech/wallet 0.1.43 → 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +17 -0
- package/.openclaw/extensions/agent-wallet/index.ts +99 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +6 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +79 -0
- package/README.md +16 -2
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +686 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +22 -0
- package/agent-wallet/agent_wallet/providers/jupiter.py +5 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +6 -0
- package/agent-wallet/agent_wallet/telemetry.py +306 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +79 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +14 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +404 -0
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_agent_wallet.py +33 -8
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/README.md +30 -1
- package/claude-code/plugins/agent-wallet/commands/wallet-setup.md +24 -0
- package/claude-code/plugins/agent-wallet/hooks/hooks.json +16 -0
- package/claude-code/plugins/agent-wallet/scripts/bootstrap_backend.sh +136 -0
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +6 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +5 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/hermes/plugins/agent_wallet/tools.py +3 -0
- package/package.json +2 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/README.md +31 -0
- package/wdk-evm-wallet/package-lock.json +268 -64
- package/wdk-evm-wallet/package.json +4 -1
- package/wdk-evm-wallet/src/config.js +2 -0
- package/wdk-evm-wallet/src/server.js +66 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +2725 -939
|
@@ -11,6 +11,12 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
13
13
|
|
|
14
|
+
try: # telemetry is optional and must never break the CLI
|
|
15
|
+
from agent_wallet.telemetry import record as _telemetry_record
|
|
16
|
+
except Exception: # pragma: no cover - defensive
|
|
17
|
+
def _telemetry_record(*_args: Any, **_kwargs: Any) -> None:
|
|
18
|
+
return None
|
|
19
|
+
|
|
14
20
|
|
|
15
21
|
def _parse_bool(value: Any) -> str:
|
|
16
22
|
if value is None:
|
|
@@ -610,6 +616,15 @@ def main() -> int:
|
|
|
610
616
|
)
|
|
611
617
|
)
|
|
612
618
|
except Exception as exc:
|
|
619
|
+
# Anonymous adoption telemetry for tool invocations only (never for
|
|
620
|
+
# onboard/wallet-create/unlock/import — those touch secrets). Records
|
|
621
|
+
# just the tool name + backend family + failure flag; never raises.
|
|
622
|
+
if getattr(args, "command", "") == "invoke":
|
|
623
|
+
_telemetry_record(
|
|
624
|
+
getattr(args, "tool", ""),
|
|
625
|
+
backend=str(locals().get("config", {}).get("backend", "") or ""),
|
|
626
|
+
ok=False,
|
|
627
|
+
)
|
|
613
628
|
error_payload: dict[str, Any] = {"ok": False, "error": str(exc)}
|
|
614
629
|
if isinstance(exc, WalletBackendError):
|
|
615
630
|
if exc.code:
|
|
@@ -619,6 +634,13 @@ def main() -> int:
|
|
|
619
634
|
print(json.dumps(error_payload), file=sys.stderr)
|
|
620
635
|
return 1
|
|
621
636
|
|
|
637
|
+
if getattr(args, "command", "") == "invoke":
|
|
638
|
+
_telemetry_record(
|
|
639
|
+
getattr(args, "tool", ""),
|
|
640
|
+
backend=str(config.get("backend", "") or ""),
|
|
641
|
+
ok=True,
|
|
642
|
+
)
|
|
643
|
+
|
|
622
644
|
print(json.dumps(payload))
|
|
623
645
|
return 0
|
|
624
646
|
|
|
@@ -117,6 +117,7 @@ async def fetch_quote(
|
|
|
117
117
|
restrict_intermediate_tokens: bool = True,
|
|
118
118
|
only_direct_routes: bool = False,
|
|
119
119
|
swap_mode: str = "ExactIn",
|
|
120
|
+
exclude_dexes: list[str] | None = None,
|
|
120
121
|
) -> dict[str, Any]:
|
|
121
122
|
"""Fetch a Jupiter quote for an exact-in swap.
|
|
122
123
|
|
|
@@ -133,6 +134,7 @@ async def fetch_quote(
|
|
|
133
134
|
restrict_intermediate_tokens=restrict_intermediate_tokens,
|
|
134
135
|
only_direct_routes=only_direct_routes,
|
|
135
136
|
swap_mode=swap_mode,
|
|
137
|
+
exclude_dexes=exclude_dexes,
|
|
136
138
|
)
|
|
137
139
|
except ProviderError as exc:
|
|
138
140
|
error_msg = str(exc).lower()
|
|
@@ -168,6 +170,7 @@ async def _fetch_quote_direct(
|
|
|
168
170
|
restrict_intermediate_tokens: bool = True,
|
|
169
171
|
only_direct_routes: bool = False,
|
|
170
172
|
swap_mode: str = "ExactIn",
|
|
173
|
+
exclude_dexes: list[str] | None = None,
|
|
171
174
|
) -> dict[str, Any]:
|
|
172
175
|
"""Fetch a Jupiter quote directly from Jupiter API."""
|
|
173
176
|
client = get_client()
|
|
@@ -180,6 +183,8 @@ async def _fetch_quote_direct(
|
|
|
180
183
|
"restrictIntermediateTokens": str(restrict_intermediate_tokens).lower(),
|
|
181
184
|
"onlyDirectRoutes": str(only_direct_routes).lower(),
|
|
182
185
|
}
|
|
186
|
+
if exclude_dexes:
|
|
187
|
+
params["excludeDexes"] = ",".join(str(d).strip() for d in exclude_dexes if str(d).strip())
|
|
183
188
|
response = await client.get(
|
|
184
189
|
f"{settings.jupiter_api_base_url.rstrip('/')}/quote",
|
|
185
190
|
params=params,
|
|
@@ -18,6 +18,12 @@ LONG_RUNNING_POST_PATHS = {
|
|
|
18
18
|
"/v1/evm/aave/withdraw/send",
|
|
19
19
|
"/v1/evm/aave/borrow/send",
|
|
20
20
|
"/v1/evm/aave/repay/send",
|
|
21
|
+
"/v1/evm/morpho/vault/supply/send",
|
|
22
|
+
"/v1/evm/morpho/vault/withdraw/send",
|
|
23
|
+
"/v1/evm/morpho/market/supply_collateral/send",
|
|
24
|
+
"/v1/evm/morpho/market/borrow/send",
|
|
25
|
+
"/v1/evm/morpho/market/repay/send",
|
|
26
|
+
"/v1/evm/morpho/market/withdraw_collateral/send",
|
|
21
27
|
"/v1/evm/lido/stake_eth_for_wsteth/send",
|
|
22
28
|
"/v1/evm/lido/wrap_steth/send",
|
|
23
29
|
"/v1/evm/lido/unwrap_wsteth/send",
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Anonymous, privacy-first adoption telemetry for the AgentLayer wallet.
|
|
2
|
+
|
|
3
|
+
Why this exists: we want to know how many people use the wallet and through which
|
|
4
|
+
host (Claude Code / Codex / Hermes / OpenClaw) — nothing more. It deliberately
|
|
5
|
+
records NO PII: no wallet addresses, balances, amounts, tx hashes, tool
|
|
6
|
+
arguments, or secrets. Only a random local install id, the host name, the
|
|
7
|
+
invoked tool's registered name, the backend family, the plugin version, and a
|
|
8
|
+
success flag.
|
|
9
|
+
|
|
10
|
+
Design for a short-lived CLI subprocess:
|
|
11
|
+
- `record()` appends one JSON line to a local spool file. This is instant and
|
|
12
|
+
durable: a single small append is atomic on POSIX, and the event survives
|
|
13
|
+
even if this process exits before any network call completes.
|
|
14
|
+
- A throttled, best-effort flush claims the spool (atomic rename), POSTs each
|
|
15
|
+
event to the provider-gateway, and re-spools anything that failed. Because
|
|
16
|
+
durability lives in the spool, a killed flush never loses data — the next
|
|
17
|
+
invocation (or a longer-lived process) ships it.
|
|
18
|
+
|
|
19
|
+
Everything here swallows its own errors. Telemetry must never slow down or break
|
|
20
|
+
a wallet operation. Users opt out with AGENT_WALLET_NO_TELEMETRY=1.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
import urllib.request
|
|
31
|
+
import uuid
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from . import __version__
|
|
36
|
+
from .config import DEFAULT_PROVIDER_GATEWAY_URL, resolve_openclaw_home
|
|
37
|
+
|
|
38
|
+
ALLOWED_HOSTS = {"claude-code", "codex", "hermes", "openclaw"}
|
|
39
|
+
|
|
40
|
+
SPOOL_NAME = "telemetry_spool.jsonl"
|
|
41
|
+
ID_NAME = "telemetry_id"
|
|
42
|
+
LAST_FLUSH_NAME = "telemetry_last_flush"
|
|
43
|
+
|
|
44
|
+
# Keep the spool bounded if the gateway is unreachable for a long time.
|
|
45
|
+
MAX_SPOOL_LINES = 500
|
|
46
|
+
# Don't attempt a network flush more than this often (seconds), unless the spool
|
|
47
|
+
# has grown past the soft cap below.
|
|
48
|
+
FLUSH_THROTTLE_SECONDS = 20
|
|
49
|
+
FLUSH_FORCE_LINES = 50
|
|
50
|
+
# Tight network bounds: telemetry never blocks meaningfully.
|
|
51
|
+
HTTP_TIMEOUT_SECONDS = 1.5
|
|
52
|
+
MAX_EVENTS_PER_FLUSH = 100
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _enabled() -> bool:
|
|
56
|
+
raw = os.getenv("AGENT_WALLET_NO_TELEMETRY", "").strip().lower()
|
|
57
|
+
return raw not in ("1", "true", "yes", "on")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _home() -> Path:
|
|
61
|
+
return resolve_openclaw_home()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _gateway_url() -> str:
|
|
65
|
+
url = os.getenv("PROVIDER_GATEWAY_URL", "").strip() or DEFAULT_PROVIDER_GATEWAY_URL
|
|
66
|
+
return url.rstrip("/")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _host() -> str:
|
|
70
|
+
host = os.getenv("AGENT_WALLET_HOST", "").strip().lower()
|
|
71
|
+
return host if host in ALLOWED_HOSTS else "unknown"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _install_id() -> str:
|
|
75
|
+
"""Stable, random, non-identifying id for this install. Created once."""
|
|
76
|
+
path = _home() / ID_NAME
|
|
77
|
+
try:
|
|
78
|
+
existing = path.read_text(encoding="utf-8").strip()
|
|
79
|
+
if existing:
|
|
80
|
+
return existing
|
|
81
|
+
except OSError:
|
|
82
|
+
pass
|
|
83
|
+
new_id = uuid.uuid4().hex
|
|
84
|
+
try:
|
|
85
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
path.write_text(new_id, encoding="utf-8")
|
|
87
|
+
except OSError:
|
|
88
|
+
pass
|
|
89
|
+
return new_id
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def record(
|
|
93
|
+
tool: str,
|
|
94
|
+
*,
|
|
95
|
+
backend: str = "",
|
|
96
|
+
ok: bool = True,
|
|
97
|
+
event: str = "tool_invoke",
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Append an anonymous event to the spool and best-effort flush. Never raises."""
|
|
100
|
+
try:
|
|
101
|
+
if not _enabled():
|
|
102
|
+
return
|
|
103
|
+
payload = {
|
|
104
|
+
"event": event,
|
|
105
|
+
"install_id": _install_id(),
|
|
106
|
+
"host": _host(),
|
|
107
|
+
"tool": tool or "",
|
|
108
|
+
"backend": backend or "",
|
|
109
|
+
"plugin_version": __version__,
|
|
110
|
+
"ok": bool(ok),
|
|
111
|
+
"ts": int(time.time()),
|
|
112
|
+
}
|
|
113
|
+
_append_spool(payload)
|
|
114
|
+
_maybe_spawn_flush()
|
|
115
|
+
except Exception:
|
|
116
|
+
# Telemetry is never allowed to affect the wallet call.
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# --- spool I/O --------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _spool_path() -> Path:
|
|
124
|
+
return _home() / SPOOL_NAME
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _append_spool(payload: dict[str, Any]) -> None:
|
|
128
|
+
path = _spool_path()
|
|
129
|
+
line = json.dumps(payload, separators=(",", ":")) + "\n"
|
|
130
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
# A single write of a short line is atomic across processes on POSIX.
|
|
132
|
+
with open(path, "a", encoding="utf-8") as fh:
|
|
133
|
+
fh.write(line)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _spool_line_count() -> int:
|
|
137
|
+
try:
|
|
138
|
+
with open(_spool_path(), "r", encoding="utf-8") as fh:
|
|
139
|
+
return sum(1 for _ in fh)
|
|
140
|
+
except OSError:
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _should_flush_now() -> bool:
|
|
145
|
+
count = _spool_line_count()
|
|
146
|
+
if count == 0:
|
|
147
|
+
return False
|
|
148
|
+
if count >= FLUSH_FORCE_LINES:
|
|
149
|
+
return True
|
|
150
|
+
last_path = _home() / LAST_FLUSH_NAME
|
|
151
|
+
try:
|
|
152
|
+
last = float(last_path.read_text(encoding="utf-8").strip() or "0")
|
|
153
|
+
except (OSError, ValueError):
|
|
154
|
+
last = 0.0
|
|
155
|
+
return (time.time() - last) >= FLUSH_THROTTLE_SECONDS
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _mark_flush_attempt() -> None:
|
|
159
|
+
try:
|
|
160
|
+
(_home() / LAST_FLUSH_NAME).write_text(str(time.time()), encoding="utf-8")
|
|
161
|
+
except OSError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _maybe_spawn_flush() -> None:
|
|
166
|
+
"""Trigger a flush in a DETACHED subprocess that outlives this process.
|
|
167
|
+
|
|
168
|
+
A per-tool CLI invocation exits microseconds after recording, so a thread
|
|
169
|
+
would be killed mid-flush. A detached child (its own session, no wait) runs
|
|
170
|
+
the network call on its own time and re-spools failures reliably. Throttling
|
|
171
|
+
keeps this to roughly one spawn per FLUSH_THROTTLE_SECONDS, so most calls
|
|
172
|
+
just append to the spool and return.
|
|
173
|
+
"""
|
|
174
|
+
if not _should_flush_now():
|
|
175
|
+
return
|
|
176
|
+
_mark_flush_attempt()
|
|
177
|
+
try:
|
|
178
|
+
subprocess.Popen(
|
|
179
|
+
[sys.executable, "-m", "agent_wallet.telemetry", "--flush"],
|
|
180
|
+
stdout=subprocess.DEVNULL,
|
|
181
|
+
stderr=subprocess.DEVNULL,
|
|
182
|
+
stdin=subprocess.DEVNULL,
|
|
183
|
+
start_new_session=True, # detach so parent exit doesn't kill it
|
|
184
|
+
)
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _pid_alive(pid: int) -> bool:
|
|
190
|
+
try:
|
|
191
|
+
os.kill(pid, 0)
|
|
192
|
+
except ProcessLookupError:
|
|
193
|
+
return False
|
|
194
|
+
except PermissionError:
|
|
195
|
+
return True # exists but owned by another user
|
|
196
|
+
except OSError:
|
|
197
|
+
return True
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _claim_suffix_pid(path: Path) -> int | None:
|
|
202
|
+
# ...telemetry_spool.jsonl.flushing.<pid>
|
|
203
|
+
try:
|
|
204
|
+
return int(path.name.rsplit(".flushing.", 1)[1])
|
|
205
|
+
except (IndexError, ValueError):
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _flush() -> None:
|
|
210
|
+
"""Claim the spool atomically, adopt dead-PID orphans, POST, re-spool failures."""
|
|
211
|
+
spool = _spool_path()
|
|
212
|
+
home = _home()
|
|
213
|
+
claim = spool.with_suffix(spool.suffix + f".flushing.{os.getpid()}")
|
|
214
|
+
|
|
215
|
+
claims: list[Path] = []
|
|
216
|
+
try:
|
|
217
|
+
os.rename(spool, claim) # atomic; only one process wins the live spool
|
|
218
|
+
claims.append(claim)
|
|
219
|
+
except OSError:
|
|
220
|
+
pass # nothing to flush, or another process claimed it
|
|
221
|
+
|
|
222
|
+
# Recover orphans left by a flush that died mid-run (crash/exit after the
|
|
223
|
+
# rename but before re-spooling). Only adopt claims whose PID is gone so we
|
|
224
|
+
# never steal a batch a live sibling is still working.
|
|
225
|
+
try:
|
|
226
|
+
for orphan in home.glob(SPOOL_NAME + ".flushing.*"):
|
|
227
|
+
if orphan == claim:
|
|
228
|
+
continue
|
|
229
|
+
pid = _claim_suffix_pid(orphan)
|
|
230
|
+
if pid is None or not _pid_alive(pid):
|
|
231
|
+
claims.append(orphan)
|
|
232
|
+
except OSError:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
if not claims:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
lines: list[str] = []
|
|
239
|
+
for c in claims:
|
|
240
|
+
try:
|
|
241
|
+
lines.extend(c.read_text(encoding="utf-8").splitlines())
|
|
242
|
+
except OSError:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
# Bound a long backlog (gateway down for a while): keep the most recent.
|
|
246
|
+
if len(lines) > MAX_SPOOL_LINES:
|
|
247
|
+
lines = lines[-MAX_SPOOL_LINES:]
|
|
248
|
+
|
|
249
|
+
url = _gateway_url() + "/v1/telemetry"
|
|
250
|
+
failed: list[str] = []
|
|
251
|
+
sent = 0
|
|
252
|
+
for line in lines:
|
|
253
|
+
line = line.strip()
|
|
254
|
+
if not line:
|
|
255
|
+
continue
|
|
256
|
+
if sent >= MAX_EVENTS_PER_FLUSH:
|
|
257
|
+
failed.append(line) # defer the rest to the next flush
|
|
258
|
+
continue
|
|
259
|
+
if _post(url, line):
|
|
260
|
+
sent += 1
|
|
261
|
+
else:
|
|
262
|
+
failed.append(line)
|
|
263
|
+
|
|
264
|
+
# Re-spool failures BEFORE removing claims: favors at-least-once delivery
|
|
265
|
+
# (a crash here re-sends, never drops). Successes are gone from the claims.
|
|
266
|
+
if failed:
|
|
267
|
+
try:
|
|
268
|
+
with open(spool, "a", encoding="utf-8") as fh:
|
|
269
|
+
fh.write("\n".join(failed) + "\n")
|
|
270
|
+
except OSError:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
for c in claims:
|
|
274
|
+
try:
|
|
275
|
+
c.unlink()
|
|
276
|
+
except OSError:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _post(url: str, body: str) -> bool:
|
|
281
|
+
try:
|
|
282
|
+
req = urllib.request.Request(
|
|
283
|
+
url,
|
|
284
|
+
data=body.encode("utf-8"),
|
|
285
|
+
headers={"Content-Type": "application/json"},
|
|
286
|
+
method="POST",
|
|
287
|
+
)
|
|
288
|
+
with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
|
|
289
|
+
return 200 <= resp.status < 300
|
|
290
|
+
except Exception:
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _flush_main() -> int:
|
|
295
|
+
"""Entry point for the detached flush subprocess (`-m agent_wallet.telemetry --flush`)."""
|
|
296
|
+
try:
|
|
297
|
+
_flush()
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
if __name__ == "__main__":
|
|
304
|
+
if "--flush" in sys.argv[1:]:
|
|
305
|
+
raise SystemExit(_flush_main())
|
|
306
|
+
raise SystemExit(0)
|
|
@@ -120,6 +120,35 @@ class AgentWalletBackend(ABC):
|
|
|
120
120
|
async def get_evm_lido_withdrawal_requests(self) -> dict[str, Any]:
|
|
121
121
|
raise WalletBackendError(f"{self.name} does not support EVM Lido withdrawal lookup.")
|
|
122
122
|
|
|
123
|
+
async def get_evm_morpho_vaults(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
vault_address: str | None = None,
|
|
127
|
+
limit: int | None = None,
|
|
128
|
+
listed_only: bool = True,
|
|
129
|
+
asset_address: str | None = None,
|
|
130
|
+
order_by: str | None = None,
|
|
131
|
+
order_direction: str | None = None,
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
raise WalletBackendError(f"{self.name} does not support EVM Morpho vault lookup.")
|
|
134
|
+
|
|
135
|
+
async def get_evm_morpho_markets(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
market_id: str | None = None,
|
|
139
|
+
limit: int | None = None,
|
|
140
|
+
listed_only: bool = True,
|
|
141
|
+
search: str | None = None,
|
|
142
|
+
collateral_asset_address: str | None = None,
|
|
143
|
+
loan_asset_address: str | None = None,
|
|
144
|
+
order_by: str | None = None,
|
|
145
|
+
order_direction: str | None = None,
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
raise WalletBackendError(f"{self.name} does not support EVM Morpho market lookup.")
|
|
148
|
+
|
|
149
|
+
async def get_evm_morpho_positions(self) -> dict[str, Any]:
|
|
150
|
+
raise WalletBackendError(f"{self.name} does not support EVM Morpho positions lookup.")
|
|
151
|
+
|
|
123
152
|
async def preview_evm_aave_operation(
|
|
124
153
|
self,
|
|
125
154
|
*,
|
|
@@ -175,6 +204,56 @@ class AgentWalletBackend(ABC):
|
|
|
175
204
|
) -> dict[str, Any]:
|
|
176
205
|
raise WalletBackendError(f"{self.name} does not support EVM Lido withdrawals.")
|
|
177
206
|
|
|
207
|
+
async def preview_evm_morpho_vault_operation(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
operation: str,
|
|
211
|
+
token_address: str,
|
|
212
|
+
vault_address: str | None = None,
|
|
213
|
+
vault_preset: str | None = None,
|
|
214
|
+
amount_raw: str | None = None,
|
|
215
|
+
native_amount_raw: str | None = None,
|
|
216
|
+
) -> dict[str, Any]:
|
|
217
|
+
raise WalletBackendError(f"{self.name} does not support EVM Morpho vault previews.")
|
|
218
|
+
|
|
219
|
+
async def send_evm_morpho_vault_operation(
|
|
220
|
+
self,
|
|
221
|
+
*,
|
|
222
|
+
operation: str,
|
|
223
|
+
token_address: str,
|
|
224
|
+
vault_address: str | None = None,
|
|
225
|
+
vault_preset: str | None = None,
|
|
226
|
+
amount_raw: str | None = None,
|
|
227
|
+
native_amount_raw: str | None = None,
|
|
228
|
+
expected_quote_fingerprint: str | None = None,
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
raise WalletBackendError(f"{self.name} does not support EVM Morpho vault operations.")
|
|
231
|
+
|
|
232
|
+
async def preview_evm_morpho_market_operation(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
operation: str,
|
|
236
|
+
token_address: str,
|
|
237
|
+
market_id: str | None = None,
|
|
238
|
+
market_preset: str | None = None,
|
|
239
|
+
amount_raw: str | None = None,
|
|
240
|
+
native_amount_raw: str | None = None,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
raise WalletBackendError(f"{self.name} does not support EVM Morpho market previews.")
|
|
243
|
+
|
|
244
|
+
async def send_evm_morpho_market_operation(
|
|
245
|
+
self,
|
|
246
|
+
*,
|
|
247
|
+
operation: str,
|
|
248
|
+
token_address: str,
|
|
249
|
+
market_id: str | None = None,
|
|
250
|
+
market_preset: str | None = None,
|
|
251
|
+
amount_raw: str | None = None,
|
|
252
|
+
native_amount_raw: str | None = None,
|
|
253
|
+
expected_quote_fingerprint: str | None = None,
|
|
254
|
+
) -> dict[str, Any]:
|
|
255
|
+
raise WalletBackendError(f"{self.name} does not support EVM Morpho market operations.")
|
|
256
|
+
|
|
178
257
|
async def preview_evm_swap(
|
|
179
258
|
self,
|
|
180
259
|
*,
|
|
@@ -4704,6 +4704,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4704
4704
|
amount_ui: float,
|
|
4705
4705
|
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
4706
4706
|
exclude_routers: list[str] | None = None,
|
|
4707
|
+
exclude_dexes: list[str] | None = None,
|
|
4707
4708
|
) -> dict[str, Any]:
|
|
4708
4709
|
if self.network != "mainnet":
|
|
4709
4710
|
raise WalletBackendError("Provider-routed swaps are only enabled for Solana mainnet.")
|
|
@@ -4749,6 +4750,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4749
4750
|
output_mint=output_mint,
|
|
4750
4751
|
amount_raw=raw_amount,
|
|
4751
4752
|
slippage_bps=slippage_bps,
|
|
4753
|
+
exclude_dexes=exclude_dexes,
|
|
4752
4754
|
)
|
|
4753
4755
|
quote_source = "jupiter-metis"
|
|
4754
4756
|
|
|
@@ -5006,17 +5008,27 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5006
5008
|
|
|
5007
5009
|
attempts: list[dict[str, Any]] = []
|
|
5008
5010
|
last_error: str | None = None
|
|
5011
|
+
_simulation_failed = False
|
|
5009
5012
|
for attempt_index in range(max_attempts):
|
|
5010
5013
|
if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
|
|
5011
5014
|
break
|
|
5012
5015
|
try:
|
|
5013
5016
|
exclude_routers = ["jupiterz"] if attempt_index > 0 else None
|
|
5017
|
+
# On retries after a simulation failure, exclude DEXes known to fail
|
|
5018
|
+
# simulation for Token-2022 tokens with extensions such as
|
|
5019
|
+
# scaledUiAmountConfig, pausableConfig, or permanentDelegate
|
|
5020
|
+
# (e.g. Backpack xStock tokens). GoonFi V2 is the primary offender;
|
|
5021
|
+
# ZeroFi handles these tokens correctly.
|
|
5022
|
+
exclude_dexes: list[str] | None = None
|
|
5023
|
+
if _simulation_failed and attempt_index > 0:
|
|
5024
|
+
exclude_dexes = ["GoonFi V2"]
|
|
5014
5025
|
preview = await self.preview_swap(
|
|
5015
5026
|
input_mint=input_mint,
|
|
5016
5027
|
output_mint=output_mint,
|
|
5017
5028
|
amount_ui=amount_ui,
|
|
5018
5029
|
slippage_bps=slippage_bps,
|
|
5019
5030
|
exclude_routers=exclude_routers,
|
|
5031
|
+
exclude_dexes=exclude_dexes,
|
|
5020
5032
|
)
|
|
5021
5033
|
estimated_output_raw = int(preview.get("estimated_output_amount_raw") or 0)
|
|
5022
5034
|
if (
|
|
@@ -5073,6 +5085,8 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5073
5085
|
return result
|
|
5074
5086
|
except (WalletBackendError, ProviderError) as exc:
|
|
5075
5087
|
last_error = str(exc)
|
|
5088
|
+
if "simulation failed" in str(exc).lower():
|
|
5089
|
+
_simulation_failed = True
|
|
5076
5090
|
attempts.append(
|
|
5077
5091
|
{
|
|
5078
5092
|
"attempt": attempt_index + 1,
|