@agentlayer.tech/wallet 0.1.36 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +37 -0
- package/README.md +8 -0
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/evm_user_wallets.py +114 -6
- package/agent-wallet/agent_wallet/openclaw_adapter.py +331 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +2 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +32 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +156 -0
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +20 -2
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +14 -7
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +3 -2
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/README.md +25 -0
- package/wdk-evm-wallet/package.json +4 -2
- package/wdk-evm-wallet/src/config.js +47 -0
- package/wdk-evm-wallet/src/server.js +25 -3
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +545 -0
|
@@ -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.
|
|
5
|
+
"version": "0.1.43",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -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
|
-
|
|
141
|
-
|
|
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.
|
|
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
|
-
|
|
5
|
-
|
|
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.
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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,8 +1,9 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
set -eu
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
package/package.json
CHANGED
package/wdk-evm-wallet/README.md
CHANGED
|
@@ -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.
|
|
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:
|
|
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);
|