@agentlayer.tech/wallet 0.1.36 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1382,6 +1382,162 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1382
1382
  "source": "wdk-evm-wallet",
1383
1383
  }
1384
1384
 
1385
+ def _normalize_uniswap_quote_payload(self, data: dict[str, Any], token_in: str, token_out: str, amount_in_raw: str) -> dict[str, Any]:
1386
+ return {
1387
+ "chain": self.chain,
1388
+ "network": self.network,
1389
+ "address": str(data.get("address") or ""),
1390
+ "token_in": str(data.get("tokenIn") or token_in),
1391
+ "token_out": str(data.get("tokenOut") or token_out),
1392
+ "amount_in_raw": str(data.get("tokenInAmount") or amount_in_raw),
1393
+ "amount_in_ui": str(data.get("inputAmountFormatted")) if data.get("inputAmountFormatted") is not None else None,
1394
+ "estimated_output_amount_raw": str(data.get("outputAmount") or "0"),
1395
+ "estimated_output_amount_ui": str(data.get("outputAmountFormatted")) if data.get("outputAmountFormatted") is not None else None,
1396
+ "minimum_output_amount_raw": str(data.get("minimumOutputAmountRaw")) if data.get("minimumOutputAmountRaw") is not None else None,
1397
+ "slippage_bps": int(data.get("slippageBps")) if data.get("slippageBps") is not None else None,
1398
+ "protocol": str(data.get("protocol") or "uniswap"),
1399
+ "routing": str(data.get("routing") or "CLASSIC"),
1400
+ "permit_required": bool(data.get("permitRequired")),
1401
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
1402
+ "router": str(data.get("router") or "").strip() or None,
1403
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
1404
+ "gas_fee_usd": str(data.get("gasFeeUSD")) if data.get("gasFeeUSD") is not None else None,
1405
+ "estimated_fee_wei": str(data.get("estimatedFeeWei")) if data.get("estimatedFeeWei") is not None else None,
1406
+ "execution_supported": bool(data.get("executionSupported")) and not self.sign_only,
1407
+ "token_in_metadata": _normalize_token_metadata(data.get("tokenInMetadata"), token_in),
1408
+ "token_out_metadata": _normalize_token_metadata(data.get("tokenOutMetadata"), token_out),
1409
+ "chain_id": int(data.get("chainId") or 0),
1410
+ "source": "wdk-evm-wallet",
1411
+ }
1412
+
1413
+ async def get_uniswap_swap_quote(
1414
+ self,
1415
+ *,
1416
+ token_in: str,
1417
+ token_out: str,
1418
+ amount_in_raw: str,
1419
+ slippage_bps: int | None = None,
1420
+ ) -> dict[str, Any]:
1421
+ resolved_address = await self.get_address()
1422
+ body: dict[str, Any] = {
1423
+ "walletId": self.wallet_id,
1424
+ "address": resolved_address,
1425
+ "accountIndex": self.account_index,
1426
+ "network": self.network,
1427
+ "tokenIn": token_in,
1428
+ "tokenOut": token_out,
1429
+ "tokenInAmount": amount_in_raw,
1430
+ }
1431
+ if slippage_bps is not None:
1432
+ body["slippageBps"] = slippage_bps
1433
+ data = await self.client.post("/v1/evm/uniswap/swap/quote", body)
1434
+ return self._normalize_uniswap_quote_payload(data, token_in, token_out, amount_in_raw)
1435
+
1436
+ async def preview_uniswap_swap(
1437
+ self,
1438
+ *,
1439
+ token_in: str,
1440
+ token_out: str,
1441
+ amount_in_raw: str,
1442
+ slippage_bps: int | None = None,
1443
+ ) -> dict[str, Any]:
1444
+ resolved_address = await self.get_address()
1445
+ body: dict[str, Any] = {
1446
+ "walletId": self.wallet_id,
1447
+ "address": resolved_address,
1448
+ "accountIndex": self.account_index,
1449
+ "network": self.network,
1450
+ "tokenIn": token_in,
1451
+ "tokenOut": token_out,
1452
+ "tokenInAmount": amount_in_raw,
1453
+ }
1454
+ if slippage_bps is not None:
1455
+ body["slippageBps"] = slippage_bps
1456
+ data = await self.client.post("/v1/evm/uniswap/swap/quote", body)
1457
+ payload = self._normalize_uniswap_quote_payload(data, token_in, token_out, amount_in_raw)
1458
+ return {
1459
+ **payload,
1460
+ "asset_type": "evm-uniswap-swap",
1461
+ "asset": "ERC20",
1462
+ "wallet": self.wallet_id,
1463
+ "from_address": resolved_address,
1464
+ "input_amount_raw": payload["amount_in_raw"],
1465
+ "input_amount_ui": payload["amount_in_ui"],
1466
+ "swap_provider": payload["protocol"],
1467
+ "route_plan": None,
1468
+ "swap_transaction": {
1469
+ "to": payload["router"],
1470
+ "value": "0",
1471
+ "data_hash": None,
1472
+ },
1473
+ }
1474
+
1475
+ async def send_uniswap_swap(
1476
+ self,
1477
+ *,
1478
+ token_in: str,
1479
+ token_out: str,
1480
+ amount_in_raw: str,
1481
+ slippage_bps: int | None = None,
1482
+ expected_quote_fingerprint: str | None = None,
1483
+ minimum_output_amount_raw: str | None = None,
1484
+ ) -> dict[str, Any]:
1485
+ if self.sign_only:
1486
+ raise WalletBackendError("wdk_evm_local is configured as sign_only.")
1487
+ body: dict[str, Any] = {
1488
+ "walletId": self.wallet_id,
1489
+ "accountIndex": self.account_index,
1490
+ "network": self.network,
1491
+ "tokenIn": token_in,
1492
+ "tokenOut": token_out,
1493
+ "tokenInAmount": amount_in_raw,
1494
+ }
1495
+ if slippage_bps is not None:
1496
+ body["slippageBps"] = slippage_bps
1497
+ if isinstance(expected_quote_fingerprint, str) and expected_quote_fingerprint.strip():
1498
+ body["expectedQuoteFingerprint"] = expected_quote_fingerprint.strip()
1499
+ if isinstance(minimum_output_amount_raw, str) and minimum_output_amount_raw.strip():
1500
+ body["minimumTokenOutAmount"] = minimum_output_amount_raw.strip()
1501
+ data = await self.client.post("/v1/evm/uniswap/swap/send", body)
1502
+ result = dict(data.get("result") or {})
1503
+ return {
1504
+ "chain": self.chain,
1505
+ "network": self.network,
1506
+ "asset_type": "evm-uniswap-swap",
1507
+ "asset": "ERC20",
1508
+ "wallet": self.wallet_id,
1509
+ "from_address": await self.get_address(),
1510
+ "token_in": str(data.get("tokenIn") or token_in),
1511
+ "token_out": str(data.get("tokenOut") or token_out),
1512
+ "input_amount_raw": str(data.get("tokenInAmount") or amount_in_raw),
1513
+ "input_amount_ui": str(data.get("inputAmountFormatted")) if data.get("inputAmountFormatted") is not None else None,
1514
+ "estimated_output_amount_raw": str(data.get("outputAmount") or "0"),
1515
+ "estimated_output_amount_ui": str(data.get("outputAmountFormatted")) if data.get("outputAmountFormatted") is not None else None,
1516
+ "slippage_bps": int(data.get("slippageBps")) if data.get("slippageBps") is not None else None,
1517
+ "minimum_output_amount_raw": str(data.get("minimumOutputAmountRaw")) if data.get("minimumOutputAmountRaw") is not None else None,
1518
+ "swap_provider": str(data.get("protocol") or "uniswap"),
1519
+ "routing": str(data.get("routing") or "CLASSIC"),
1520
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
1521
+ "router": str(data.get("router") or "").strip() or None,
1522
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
1523
+ "simulation": _normalize_swap_simulation(data.get("simulation")),
1524
+ "swap_transaction": {
1525
+ "to": str((data.get("swapTransaction") or {}).get("to") or "").strip() or None,
1526
+ "value": str((data.get("swapTransaction") or {}).get("value") or "0"),
1527
+ "data_hash": str((data.get("swapTransaction") or {}).get("dataHash") or "").strip() or None,
1528
+ },
1529
+ "token_in_metadata": _normalize_token_metadata(data.get("tokenInMetadata"), token_in),
1530
+ "token_out_metadata": _normalize_token_metadata(data.get("tokenOutMetadata"), token_out),
1531
+ "hash": result.get("hash"),
1532
+ "approve_hash": result.get("approveHash"),
1533
+ "reset_allowance_hash": result.get("resetAllowanceHash"),
1534
+ "result": result,
1535
+ "chain_id": int(data.get("chainId") or 0),
1536
+ "broadcasted": True,
1537
+ "confirmed": False,
1538
+ "source": "wdk-evm-wallet",
1539
+ }
1540
+
1385
1541
  async def preview_evm_lifi_cross_chain_swap(
1386
1542
  self,
1387
1543
  *,
@@ -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.36",
5
+ "version": "0.1.43",
6
6
  "skills": ["skills/wallet-operator"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.36"
7
+ version = "0.1.43"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -137,8 +137,25 @@ def _auto_start_local_service(
137
137
  wdk_wallet_root: Path,
138
138
  config_path: Path,
139
139
  ) -> dict[str, object]:
140
- if _service_is_healthy(service_url):
141
- return {"started": False, "already_healthy": True}
140
+ # Restart a running-but-stale daemon (old code in memory after a release) by
141
+ # comparing its /health version to the on-disk launcher version. Idempotent:
142
+ # steady state is a no-op. See agent_wallet.evm_user_wallets for the shared
143
+ # health/version/stop helpers.
144
+ from agent_wallet.evm_user_wallets import (
145
+ _read_on_disk_service_version,
146
+ _service_health,
147
+ _stop_local_service,
148
+ )
149
+
150
+ restarted = False
151
+ health = _service_health(service_url)
152
+ if health is not None:
153
+ expected_version = _read_on_disk_service_version(wdk_wallet_root)
154
+ running_version = str(health.get("version") or "").strip()
155
+ if expected_version is None or running_version == expected_version:
156
+ return {"started": False, "already_healthy": True}
157
+ _stop_local_service(service_url)
158
+ restarted = True
142
159
 
143
160
  if not _is_local_service_url(service_url):
144
161
  raise SystemExit(
@@ -178,6 +195,7 @@ def _auto_start_local_service(
178
195
  return {
179
196
  "started": True,
180
197
  "already_healthy": False,
198
+ "restarted": restarted,
181
199
  "pid": process.pid,
182
200
  "log_path": str(log_path),
183
201
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.36",
4
+ "version": "0.1.43",
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"
@@ -1,14 +1,21 @@
1
1
  #!/bin/sh
2
2
  set -eu
3
3
 
4
- SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
- PLUGIN_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
4
+ # Resolve to PHYSICAL paths (pwd -P). Claude Code runs this plugin through a
5
+ # marketplace symlink (~/.claude/agentlayer-local/plugins/agent-wallet -> the real
6
+ # plugin dir). With a logical pwd, the symlink stays in PLUGIN_ROOT and the
7
+ # "../../../codex" sibling math below collapses lexically into a non-existent path
8
+ # (e.g. ~/.claude/codex), so `cd` fails and the server dies with -32000. Resolving
9
+ # the symlink up front keeps the `..` arithmetic consistent with the real layout.
10
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
11
+ PLUGIN_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd -P)
6
12
  OPENCLAW_HOME=${OPENCLAW_HOME:-"$HOME/.openclaw"}
7
13
  PACKAGE_ROOT=${AGENT_WALLET_PACKAGE_ROOT:-${OPENCLAW_AGENT_WALLET_PACKAGE_ROOT:-"$OPENCLAW_HOME/agent-wallet-runtime/current/agent-wallet"}}
8
14
 
9
- # Resolve server.py. When Claude Code copies this plugin into its cache, the
10
- # relative sibling paths below no longer resolve, so fall back to the codex
11
- # plugin copy inside the installed runtime package, which is always present.
15
+ # Resolve server.py. The claude-code plugin ships no server.py of its own; it
16
+ # reuses the sibling codex copy (real repo / installed runtime, where claude-code
17
+ # and codex sit side by side), falling back to the codex copy inside the runtime
18
+ # package, which is always present.
12
19
  LOCAL_SERVER="$PLUGIN_ROOT/server.py"
13
20
  CODEX_SERVER="$PLUGIN_ROOT/../../../codex/plugins/agent-wallet/server.py"
14
21
  RUNTIME_CODEX_DIR="$OPENCLAW_HOME/agent-wallet-runtime/current/codex/plugins/agent-wallet"
@@ -16,9 +23,9 @@ RUNTIME_CODEX_DIR="$OPENCLAW_HOME/agent-wallet-runtime/current/codex/plugins/age
16
23
  if [ -f "$LOCAL_SERVER" ]; then
17
24
  SERVER_PY="$LOCAL_SERVER"
18
25
  elif [ -f "$CODEX_SERVER" ]; then
19
- SERVER_PY=$(CDPATH= cd -- "$PLUGIN_ROOT/../../../codex/plugins/agent-wallet" && pwd)/server.py
26
+ SERVER_PY=$(CDPATH= cd -- "$PLUGIN_ROOT/../../../codex/plugins/agent-wallet" && pwd -P)/server.py
20
27
  elif [ -f "$RUNTIME_CODEX_DIR/server.py" ]; then
21
- SERVER_PY=$(CDPATH= cd -- "$RUNTIME_CODEX_DIR" && pwd)/server.py
28
+ SERVER_PY=$(CDPATH= cd -- "$RUNTIME_CODEX_DIR" && pwd -P)/server.py
22
29
  else
23
30
  printf '{"error":"agent-wallet server.py not found in plugin, codex sibling, or runtime package.","fix":"npx @agentlayer.tech/wallet install --yes"}\n' >&2
24
31
  exit 1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
- "version": "0.1.36",
3
+ "version": "0.1.43",
4
4
  "description": "Codex plugin bridge for the AgentLayer wallet runtime.",
5
5
  "author": {
6
6
  "name": "AgentLayer"
@@ -1,8 +1,9 @@
1
1
  #!/bin/sh
2
2
  set -eu
3
3
 
4
- SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
- PLUGIN_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
4
+ # Physical paths (pwd -P) so symlinked install layouts resolve to the real dir.
5
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
6
+ PLUGIN_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd -P)
6
7
  OPENCLAW_HOME=${OPENCLAW_HOME:-"$HOME/.openclaw"}
7
8
  PACKAGE_ROOT=${AGENT_WALLET_PACKAGE_ROOT:-${OPENCLAW_AGENT_WALLET_PACKAGE_ROOT:-"$OPENCLAW_HOME/agent-wallet-runtime/current/agent-wallet"}}
8
9
 
@@ -1,5 +1,5 @@
1
1
  name: agent-wallet
2
- version: 0.1.36
2
+ version: 0.1.43
3
3
  description: Thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend
4
4
  provides_tools:
5
5
  - agent_wallet_tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.36",
3
+ "version": "0.1.43",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-btc-wallet",
3
- "version": "0.1.36",
3
+ "version": "0.1.43",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate BTC-only wallet service built on Tether WDK.",
@@ -20,6 +20,8 @@ Current scope:
20
20
  - fetch fee-rate suggestions
21
21
  - fetch read-only Velora swap quotes for supported mainnet ERC-20 and native ETH pairs
22
22
  - execute Velora ERC-20 and native ETH swaps on supported mainnet networks through the local wallet account
23
+ - fetch read-only Uniswap Trading API swap quotes (CLASSIC routing) for native ETH and ERC-20 pairs on ethereum/base
24
+ - execute Uniswap Trading API swaps (native ETH and ERC-20 inputs) with Permit2 EIP-712 signing for ERC-20 inputs
23
25
  - fetch Aave V3 account data on supported mainnet networks
24
26
  - fetch Aave V3 reserve catalog on supported mainnet networks
25
27
  - fetch Aave V3 per-reserve user positions on supported mainnet networks
@@ -101,6 +103,8 @@ The active network is persistent and can be switched without changing code.
101
103
  - `POST /v1/evm/aave/repay/send`
102
104
  - `POST /v1/evm/swap/quote`
103
105
  - `POST /v1/evm/swap/send`
106
+ - `POST /v1/evm/uniswap/swap/quote`
107
+ - `POST /v1/evm/uniswap/swap/send`
104
108
  - `POST /v1/evm/transfer/quote`
105
109
  - `POST /v1/evm/transfer/send`
106
110
  - `POST /v1/evm/token-transfer/quote`
@@ -158,6 +162,26 @@ Environment variables:
158
162
  - `WDK_EVM_SEPOLIA_RPC_URL`
159
163
  - `WDK_EVM_BASE_RPC_URL`
160
164
  - `WDK_EVM_BASE_SEPOLIA_RPC_URL`
165
+ - `UNISWAP_API_KEY`
166
+ - `UNISWAP_TRADING_API_BASE_URL`
167
+ - `UNISWAP_ROUTER_VERSION`
168
+ - `UNISWAP_DEFAULT_SLIPPAGE_BPS`
169
+
170
+ Swap providers:
171
+
172
+ - the runtime exposes three independent swap surfaces: Velora (`/v1/evm/swap/*`),
173
+ LI.FI cross-chain (`/v1/evm/lifi/*`), and Uniswap Trading API
174
+ (`/v1/evm/uniswap/swap/*`) — always keep more than one route available
175
+ - Uniswap Trading API support is limited to `ethereum` and `base`, `EXACT_INPUT`,
176
+ and CLASSIC routing only; non-CLASSIC quotes (UniswapX Dutch/Priority) are rejected
177
+ - native ETH inputs need no approval or signature; ERC-20 inputs are pulled via
178
+ Permit2 (`0x000000000022D473030F116dDEE9F6B43aC78BA3`) and require a per-swap
179
+ Permit2 EIP-712 signature produced locally by the wallet account
180
+ - the `/swap` response `to` address is checked against a pinned Universal Router
181
+ allow-list before broadcast, and every swap is simulated first
182
+ - `UNISWAP_API_KEY` is required for the Uniswap routes; it identifies the
183
+ integrator (this service), not an end user — swaps are scoped per request by the
184
+ active wallet address, so a single key never mixes users
161
185
 
162
186
  Gateway mode:
163
187
 
@@ -179,6 +203,7 @@ Local security note:
179
203
  - seed reveal is password-gated and separate from normal agent operations
180
204
  - Velora swap support is currently limited to `ethereum` and `base` ERC-20 and native ETH pairs
181
205
  - the underlying WDK Velora package is still beta; test swap execution carefully before relying on it
206
+ - Uniswap Trading API swaps perform a Permit2-scoped ERC-20 approval for ERC-20 inputs; if a send fails after approval, the service attempts to restore the original allowance
182
207
  - Aave V3 support is currently limited to `ethereum` and `base`
183
208
  - Aave `supply` and `repay` may perform pool-scoped ERC-20 approvals; if a send fails after approval, the service attempts to restore the original allowance
184
209
  - Aave delegated `onBehalfOf` operations and third-party withdraw destinations are intentionally not exposed in this runtime
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-evm-wallet",
3
- "version": "0.1.36",
3
+ "version": "0.1.43",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate EVM wallet service built on Tether WDK.",
@@ -11,7 +11,9 @@
11
11
  "check": "node --check src/server.js && node --check src/wdk_evm_wallet.js && node --check src/config.js && node --check src/json.js && node --check src/local_vault.js && node --check src/network_state.js",
12
12
  "test:swap-runtime": "node --test --test-concurrency=1 tests/smoke_swap_runtime.mjs",
13
13
  "test:aave-runtime": "node --test --test-concurrency=1 tests/smoke_aave_runtime.mjs",
14
- "test:lido-runtime": "node --test --test-concurrency=1 tests/smoke_lido_runtime.mjs"
14
+ "test:lido-runtime": "node --test --test-concurrency=1 tests/smoke_lido_runtime.mjs",
15
+ "test:uniswap-runtime": "node --test --test-concurrency=1 tests/smoke_uniswap_runtime.mjs",
16
+ "test:unit": "node --test tests/unit_uniswap_helpers.mjs"
15
17
  },
16
18
  "dependencies": {
17
19
  "@tetherto/wdk": "^1.0.0-beta.9",
@@ -9,6 +9,23 @@ const DEFAULTS = {
9
9
  network: "sepolia",
10
10
  unlockTimeoutSeconds: 0,
11
11
  };
12
+
13
+ // Read this package's version once at module load. Surfaced via /health so the
14
+ // host autostart can detect a stale long-running daemon (old code in memory)
15
+ // after a release and restart it. Falls back to "0.0.0" if package.json is
16
+ // unreadable — a value that will simply look "stale" and trigger a restart.
17
+ function readPackageVersion() {
18
+ try {
19
+ const pkgUrl = new URL("../package.json", import.meta.url);
20
+ const pkg = JSON.parse(fs.readFileSync(pkgUrl, "utf8"));
21
+ const version = String(pkg.version || "").trim();
22
+ return version || "0.0.0";
23
+ } catch {
24
+ return "0.0.0";
25
+ }
26
+ }
27
+
28
+ const PACKAGE_VERSION = readPackageVersion();
12
29
  const DEFAULT_PROVIDER_GATEWAY_URL = "https://agent-layer-production.up.railway.app";
13
30
  const ENFORCED_GATEWAY_MAINNETS = new Set(["ethereum", "base"]);
14
31
 
@@ -257,6 +274,20 @@ export function loadConfig(env = process.env) {
257
274
  },
258
275
  };
259
276
 
277
+ // Route Uniswap Trading API calls through the provider-gateway by default so the
278
+ // Uniswap key lives only in the gateway (never in a per-release wallet .env). When
279
+ // the base URL points at the gateway we authenticate with the gateway bearer and
280
+ // let the gateway inject x-api-key; an explicit non-gateway base URL falls back to
281
+ // the legacy direct mode (local UNISWAP_API_KEY + x-api-key).
282
+ const gatewayBaseTrimmed = String(providerGatewayUrl || "").replace(/\/+$/, "");
283
+ const uniswapTradingApiBaseUrl =
284
+ String(env.UNISWAP_TRADING_API_BASE_URL ?? "").trim() ||
285
+ (gatewayBaseTrimmed
286
+ ? `${gatewayBaseTrimmed}/v1/evm/uniswap`
287
+ : "https://trade-api.gateway.uniswap.org/v1");
288
+ const uniswapViaGateway =
289
+ Boolean(gatewayBaseTrimmed) && uniswapTradingApiBaseUrl.startsWith(gatewayBaseTrimmed);
290
+
260
291
  return {
261
292
  host,
262
293
  port: parseInteger(env.PORT, DEFAULTS.port, "PORT"),
@@ -284,6 +315,22 @@ export function loadConfig(env = process.env) {
284
315
  lifiDefaultDenyBridges: String(env.LIFI_DEFAULT_DENY_BRIDGES ?? "").trim() || "mayan",
285
316
  lidoApiBaseUrl: String(env.LIDO_API_BASE_URL ?? "").trim() || "https://eth-api.lido.fi/v1",
286
317
  lidoReferralAddress: String(env.LIDO_REFERRAL_ADDRESS ?? "").trim(),
318
+ uniswapTradingApiBaseUrl,
319
+ uniswapViaGateway,
320
+ providerGatewayToken,
321
+ uniswapApiKey: String(env.UNISWAP_API_KEY ?? "").trim(),
322
+ uniswapRouterVersion: String(env.UNISWAP_ROUTER_VERSION ?? "").trim() || "2.0",
323
+ // 300 bps (3%) default mirrors the Solana swap-intent floor. Active markets
324
+ // (Base re-prices every block) drift during the multi-step preview -> approval
325
+ // -> execute window; a 0.5% floor rejected ordinary drift. The quote
326
+ // fingerprint binds only the swap intent, so this floor — not an exact-output
327
+ // pin — is the real slippage guard. Override per-call or via env for tight swaps.
328
+ uniswapDefaultSlippageBps: parseInteger(
329
+ env.UNISWAP_DEFAULT_SLIPPAGE_BPS,
330
+ 300,
331
+ "UNISWAP_DEFAULT_SLIPPAGE_BPS"
332
+ ),
333
+ version: PACKAGE_VERSION,
287
334
  networkProfiles,
288
335
  };
289
336
  }
@@ -40,7 +40,10 @@ function normalizeErrorCode(errorCode, pathname, message) {
40
40
  code === "aave_cleanup_failed" ||
41
41
  code === "token_transfer_failed" ||
42
42
  code === "fee_limit_exceeded" ||
43
- code === "token_read_failed"
43
+ code === "token_read_failed" ||
44
+ code === "uniswap_api_key_missing" ||
45
+ code === "uniswap_unsupported_route" ||
46
+ code === "uniswap_unexpected_router"
44
47
  ) {
45
48
  return code;
46
49
  }
@@ -129,10 +132,17 @@ function errorStatusCode(errorCode, fallback = 400) {
129
132
  errorCode === "aave_fee_unavailable" ||
130
133
  errorCode === "aave_cleanup_failed" ||
131
134
  errorCode === "token_transfer_failed" ||
132
- errorCode === "fee_limit_exceeded"
135
+ errorCode === "fee_limit_exceeded" ||
136
+ errorCode === "uniswap_api_key_missing"
133
137
  ) {
134
138
  return 400;
135
139
  }
140
+ if (errorCode === "uniswap_unsupported_route") {
141
+ return 422;
142
+ }
143
+ if (errorCode === "uniswap_unexpected_router") {
144
+ return 502;
145
+ }
136
146
  if (errorCode === "token_read_failed") {
137
147
  return 502;
138
148
  }
@@ -272,7 +282,7 @@ async function handleRequest(request, response) {
272
282
  return sendJson(response, 200, {
273
283
  ok: true,
274
284
  service: "wdk-evm-wallet",
275
- version: "0.1.0",
285
+ version: config.version,
276
286
  wallet: "evm",
277
287
  network: runtimeConfig.network,
278
288
  chainId: runtimeConfig.chainId,
@@ -531,6 +541,18 @@ async function handleRequest(request, response) {
531
541
  return sendJson(response, 200, { ok: true, data });
532
542
  }
533
543
 
544
+ if (method === "POST" && url.pathname === "/v1/evm/uniswap/swap/quote") {
545
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
546
+ const data = await service.quoteUniswapSwap(body);
547
+ return sendJson(response, 200, { ok: true, data });
548
+ }
549
+
550
+ if (method === "POST" && url.pathname === "/v1/evm/uniswap/swap/send") {
551
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
552
+ const data = await service.sendUniswapSwap(body);
553
+ return sendJson(response, 200, { ok: true, data });
554
+ }
555
+
534
556
  if (method === "POST" && url.pathname === "/v1/evm/transfer/quote") {
535
557
  const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
536
558
  const data = await service.quoteNativeTransfer(body);