@agentlayer.tech/wallet 0.1.77 → 0.1.84

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.
@@ -1963,7 +1963,7 @@ const evmToolDefinitions = [
1963
1963
  },
1964
1964
  {
1965
1965
  name: "get_uniswap_swap_quote",
1966
- description: "Get a read-only Uniswap Trading API quote (CLASSIC routing) for an ERC-20 or native ETH swap on ethereum, base, or robinhood. This does not approve, sign, or execute a swap.",
1966
+ description: "Get a read-only Uniswap execution preview for an ERC-20 or native ETH swap on ethereum, base, or robinhood. Supported paths are CLASSIC, UniswapX orders, and canonical ETH↔WETH wrap/unwrap. This does not approve, sign, or execute a swap.",
1967
1967
  parameters: {
1968
1968
  type: "object",
1969
1969
  properties: {
@@ -1979,7 +1979,7 @@ const evmToolDefinitions = [
1979
1979
  },
1980
1980
  {
1981
1981
  name: "swap_evm_uniswap_tokens",
1982
- description: "Preview, prepare, or execute an ERC-20 or native ETH swap through the Uniswap Trading API (CLASSIC routing) on ethereum, base, or robinhood. ERC-20 inputs use Permit2 EIP-712 signing automatically. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
1982
+ description: "Preview, prepare, or execute a supported Uniswap path on ethereum, base, or robinhood: CLASSIC, UniswapX orders, or canonical ETH↔WETH wrap/unwrap. ERC-20 paths may use Permit2 EIP-712 or an UniswapX order signature. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
1983
1983
  optional: true,
1984
1984
  parameters: {
1985
1985
  type: "object",
@@ -1963,7 +1963,7 @@ const evmToolDefinitions = [
1963
1963
  },
1964
1964
  {
1965
1965
  name: "get_uniswap_swap_quote",
1966
- description: "Get a read-only Uniswap Trading API quote (CLASSIC routing) for an ERC-20 or native ETH swap on ethereum, base, or robinhood. This does not approve, sign, or execute a swap.",
1966
+ description: "Get a read-only Uniswap execution preview for an ERC-20 or native ETH swap on ethereum, base, or robinhood. Supported paths are CLASSIC, UniswapX orders, and canonical ETH↔WETH wrap/unwrap. This does not approve, sign, or execute a swap.",
1967
1967
  parameters: {
1968
1968
  type: "object",
1969
1969
  properties: {
@@ -1979,7 +1979,7 @@ const evmToolDefinitions = [
1979
1979
  },
1980
1980
  {
1981
1981
  name: "swap_evm_uniswap_tokens",
1982
- description: "Preview, prepare, or execute an ERC-20 or native ETH swap through the Uniswap Trading API (CLASSIC routing) on ethereum, base, or robinhood. ERC-20 inputs use Permit2 EIP-712 signing automatically. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
1982
+ description: "Preview, prepare, or execute a supported Uniswap path on ethereum, base, or robinhood: CLASSIC, UniswapX orders, or canonical ETH↔WETH wrap/unwrap. ERC-20 paths may use Permit2 EIP-712 or an UniswapX order signature. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
1983
1983
  optional: true,
1984
1984
  parameters: {
1985
1985
  type: "object",
@@ -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.77",
5
+ "version": "0.1.84",
6
6
  "contracts": {
7
7
  "tools": [
8
8
  "agentlayer_autonomous_approve",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayertech/agent-wallet-plugin",
3
- "version": "0.1.77",
3
+ "version": "0.1.84",
4
4
  "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN ../../../LICENSE",
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.77
1
+ 0.1.84
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Keep in sync with package.json, pyproject.toml, and the npm installer version.
4
4
  # scripts/check_release_version.mjs enforces this on release.
5
- __version__ = "0.1.77"
5
+ __version__ = "0.1.84"
6
6
 
7
7
  __all__ = [
8
8
  "config",
@@ -2174,8 +2174,9 @@ class OpenClawWalletAdapter:
2174
2174
  AgentToolSpec(
2175
2175
  name="get_uniswap_swap_quote",
2176
2176
  description=(
2177
- "Get a read-only Uniswap Trading API quote (CLASSIC routing) for an ERC-20 or native ETH swap "
2178
- "on ethereum, base, or robinhood. Supports full EIP-712 Permit2 path for ERC-20 inputs. "
2177
+ "Get a read-only Uniswap execution preview for an ERC-20 or native ETH swap "
2178
+ "on ethereum, base, or robinhood. Supported paths are CLASSIC, UniswapX orders, "
2179
+ "and canonical ETH↔WETH wrap/unwrap. Supports Permit2 where required. "
2179
2180
  "This does not approve, sign, or execute a swap."
2180
2181
  ),
2181
2182
  input_schema={
@@ -2215,8 +2216,9 @@ class OpenClawWalletAdapter:
2215
2216
  AgentToolSpec(
2216
2217
  name="swap_evm_uniswap_tokens",
2217
2218
  description=(
2218
- "Preview, prepare, or execute an ERC-20 or native ETH swap through the Uniswap Trading API "
2219
- "(CLASSIC routing) on ethereum, base, or robinhood. ERC-20 inputs use Permit2 EIP-712 signing automatically. "
2219
+ "Preview, prepare, or execute a supported Uniswap path on ethereum, base, or robinhood: "
2220
+ "CLASSIC, UniswapX orders, or canonical ETH↔WETH wrap/unwrap. ERC-20 paths may use Permit2 "
2221
+ "EIP-712 or an UniswapX order signature. "
2220
2222
  "Prepare returns an execution plan only. Execute requires a host-issued approval token bound to the previewed operation "
2221
2223
  "unless the high-trust autonomous permission group is enabled."
2222
2224
  ),
@@ -1835,6 +1835,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1835
1835
  "slippage_bps": int(data.get("slippageBps")) if data.get("slippageBps") is not None else None,
1836
1836
  "protocol": str(data.get("protocol") or "uniswap"),
1837
1837
  "routing": str(data.get("routing") or "CLASSIC"),
1838
+ "execution_kind": str(data.get("executionKind") or "classic"),
1838
1839
  "permit_required": bool(data.get("permitRequired")),
1839
1840
  "allowance": _normalize_swap_allowance(data.get("allowance")),
1840
1841
  "router": str(data.get("router") or "").strip() or None,
@@ -1845,7 +1846,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1845
1846
  "token_in_metadata": _normalize_token_metadata(data.get("tokenInMetadata"), token_in),
1846
1847
  "token_out_metadata": _normalize_token_metadata(data.get("tokenOutMetadata"), token_out),
1847
1848
  "chain_id": int(data.get("chainId") or 0),
1848
- "source": "wdk-evm-wallet",
1849
+ "source": str(data.get("source") or "wdk-evm-wallet"),
1849
1850
  }
1850
1851
 
1851
1852
  async def get_uniswap_swap_quote(
@@ -1893,6 +1894,19 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1893
1894
  body["slippageBps"] = slippage_bps
1894
1895
  data = await self.client.post("/v1/evm/uniswap/swap/quote", body)
1895
1896
  payload = self._normalize_uniswap_quote_payload(data, token_in, token_out, amount_in_raw)
1897
+ prepared_transaction = data.get("swapTransaction")
1898
+ if isinstance(prepared_transaction, dict):
1899
+ swap_transaction = {
1900
+ "to": str(prepared_transaction.get("to") or "").strip() or payload["router"],
1901
+ "value": str(prepared_transaction.get("value") or "0"),
1902
+ "data_hash": str(prepared_transaction.get("dataHash") or "").strip() or None,
1903
+ }
1904
+ else:
1905
+ swap_transaction = {
1906
+ "to": payload["router"],
1907
+ "value": "0",
1908
+ "data_hash": None,
1909
+ }
1896
1910
  return {
1897
1911
  **payload,
1898
1912
  "asset_type": "evm-uniswap-swap",
@@ -1903,11 +1917,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1903
1917
  "input_amount_ui": payload["amount_in_ui"],
1904
1918
  "swap_provider": payload["protocol"],
1905
1919
  "route_plan": None,
1906
- "swap_transaction": {
1907
- "to": payload["router"],
1908
- "value": "0",
1909
- "data_hash": None,
1910
- },
1920
+ "swap_transaction": swap_transaction,
1911
1921
  }
1912
1922
 
1913
1923
  async def send_uniswap_swap(
@@ -1938,6 +1948,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1938
1948
  body["minimumTokenOutAmount"] = minimum_output_amount_raw.strip()
1939
1949
  data = await self.client.post("/v1/evm/uniswap/swap/send", body)
1940
1950
  result = dict(data.get("result") or {})
1951
+ order_id = str(result.get("orderId") or "").strip() or None
1941
1952
  return {
1942
1953
  "chain": self.chain,
1943
1954
  "network": self.network,
@@ -1955,6 +1966,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1955
1966
  "minimum_output_amount_raw": str(data.get("minimumOutputAmountRaw")) if data.get("minimumOutputAmountRaw") is not None else None,
1956
1967
  "swap_provider": str(data.get("protocol") or "uniswap"),
1957
1968
  "routing": str(data.get("routing") or "CLASSIC"),
1969
+ "execution_kind": str(data.get("executionKind") or "classic"),
1958
1970
  "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
1959
1971
  "router": str(data.get("router") or "").strip() or None,
1960
1972
  "allowance": _normalize_swap_allowance(data.get("allowance")),
@@ -1967,13 +1979,16 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1967
1979
  "token_in_metadata": _normalize_token_metadata(data.get("tokenInMetadata"), token_in),
1968
1980
  "token_out_metadata": _normalize_token_metadata(data.get("tokenOutMetadata"), token_out),
1969
1981
  "hash": result.get("hash"),
1982
+ "order_id": order_id,
1983
+ "order_status": str(result.get("orderStatus") or "").strip() or None,
1970
1984
  "approve_hash": result.get("approveHash"),
1971
1985
  "reset_allowance_hash": result.get("resetAllowanceHash"),
1972
1986
  "result": result,
1973
1987
  "chain_id": int(data.get("chainId") or 0),
1974
- "broadcasted": True,
1988
+ "broadcasted": bool(result.get("hash")),
1989
+ "order_submitted": order_id is not None,
1975
1990
  "confirmed": False,
1976
- "source": "wdk-evm-wallet",
1991
+ "source": str(data.get("source") or "wdk-evm-wallet"),
1977
1992
  }
1978
1993
 
1979
1994
  async def preview_evm_lifi_cross_chain_swap(
@@ -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.77",
5
+ "version": "0.1.84",
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.77"
7
+ version = "0.1.84"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -99,7 +99,7 @@ Use this skill before calling OpenClaw wallet tools. It is the routing guide for
99
99
  - Current intended path is ERC-20 to ERC-20 on `ethereum` or `base`.
100
100
  - EVM same-chain Uniswap swap: `swap_evm_uniswap_tokens`
101
101
  - Params: `token_in`, `token_out`, `amount_in_raw` base-unit string, `mode`, `purpose`, optional `network`.
102
- - Current intended path supports `ethereum`, `base`, and `robinhood` with CLASSIC routing.
102
+ - Supported paths on `ethereum`, `base`, and `robinhood` are CLASSIC, UniswapX orders, and canonical ETH↔WETH wrap/unwrap; other routes fail closed before signing.
103
103
  - EVM swap quote only: `get_evm_swap_quote`
104
104
  - Params: `token_in`, `token_out`, `amount_in_raw`, optional `network`.
105
105
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.77",
4
+ "version": "0.1.84",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
- "version": "0.1.77",
3
+ "version": "0.1.84",
4
4
  "description": "Codex plugin bridge for the AgentLayer wallet runtime.",
5
5
  "author": {
6
6
  "name": "AgentLayer"
@@ -1,5 +1,5 @@
1
1
  name: agent-wallet
2
- version: 0.1.77
2
+ version: 0.1.84
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.77",
3
+ "version": "0.1.84",
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.77",
3
+ "version": "0.1.84",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate BTC-only wallet service built on Tether WDK.",
@@ -27,4 +27,7 @@ MORPHO_API_BASE_URL=https://api.morpho.org/graphql
27
27
  UNISWAP_API_KEY=
28
28
  # UNISWAP_TRADING_API_BASE_URL= # defaults to PROVIDER_GATEWAY_URL/v1/evm/uniswap
29
29
  UNISWAP_ROUTER_VERSION=2.0
30
+ # Optional per-network override; each value must exist in the wallet's reviewed
31
+ # execution profile and match the provider-gateway's per-chain configuration.
32
+ UNISWAP_ROUTER_VERSION_BY_NETWORK={"ethereum":"2.0","base":"2.0","robinhood":"2.0"}
30
33
  UNISWAP_DEFAULT_SLIPPAGE_BPS=300
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-evm-wallet",
3
- "version": "0.1.77",
3
+ "version": "0.1.84",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate EVM wallet service built on Tether WDK.",
@@ -189,6 +189,35 @@ function normalizeNetworkKey(value) {
189
189
  return aliases[normalized] || normalized;
190
190
  }
191
191
 
192
+ function parseUniswapRouterVersionsByNetwork(value) {
193
+ const raw = String(value ?? "").trim();
194
+ if (!raw) {
195
+ return {};
196
+ }
197
+ let parsed;
198
+ try {
199
+ parsed = JSON.parse(raw);
200
+ } catch {
201
+ throw new Error("UNISWAP_ROUTER_VERSION_BY_NETWORK must be a JSON object.");
202
+ }
203
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
204
+ throw new Error("UNISWAP_ROUTER_VERSION_BY_NETWORK must be a JSON object.");
205
+ }
206
+ const normalized = {};
207
+ for (const [network, version] of Object.entries(parsed)) {
208
+ const networkKey = normalizeNetworkKey(network);
209
+ if (!Object.hasOwn(DEFAULT_NETWORK_PROFILES, networkKey)) {
210
+ throw new Error(`UNISWAP_ROUTER_VERSION_BY_NETWORK contains unsupported network '${network}'.`);
211
+ }
212
+ const normalizedVersion = String(version ?? "").trim();
213
+ if (!normalizedVersion) {
214
+ throw new Error(`UNISWAP_ROUTER_VERSION_BY_NETWORK.${network} must be a non-empty string.`);
215
+ }
216
+ normalized[networkKey] = normalizedVersion;
217
+ }
218
+ return normalized;
219
+ }
220
+
192
221
  function joinUrl(base, pathname) {
193
222
  const normalizedBase = String(base || "").trim();
194
223
  if (!normalizedBase) {
@@ -360,6 +389,9 @@ export function loadConfig(env = process.env) {
360
389
  providerGatewayToken,
361
390
  uniswapApiKey: String(env.UNISWAP_API_KEY ?? "").trim(),
362
391
  uniswapRouterVersion: String(env.UNISWAP_ROUTER_VERSION ?? "").trim() || "2.0",
392
+ uniswapRouterVersionsByNetwork: parseUniswapRouterVersionsByNetwork(
393
+ env.UNISWAP_ROUTER_VERSION_BY_NETWORK
394
+ ),
363
395
  // 300 bps (3%) default mirrors the Solana swap-intent floor. Active markets
364
396
  // (Base re-prices every block) drift during the multi-step preview -> approval
365
397
  // -> execute window; a 0.5% floor rejected ordinary drift. The quote
@@ -23,12 +23,63 @@ const DEFAULT_LIFI_SLIPPAGE = 0.005;
23
23
  const ALWAYS_DENIED_LIFI_BRIDGES = ["mayan"];
24
24
  const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
25
25
  const UNISWAP_SUPPORTED_CHAIN_IDS = { ethereum: 1, base: 8453, robinhood: 4663 };
26
- // Universal Router v2.0 allow-list (defense-in-depth: /swap response `to` must match).
27
- const UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK = {
28
- ethereum: "0x66a9893cc07d91d95644aedd05d03f95e1dba8af",
29
- base: "0x6ff5693b99212da76ad316178a184ab56d299b43",
30
- robinhood: "0x8876789976decbfcbbbe364623c63652db8c0904",
26
+ // Every executable path is declared here rather than inferred from a Trading
27
+ // API response. A new chain/router version therefore requires an explicit,
28
+ // reviewed allow-list entry before it can receive a signed transaction.
29
+ const UNISWAP_EXECUTION_PROFILES = {
30
+ ethereum: {
31
+ chainId: 1,
32
+ universalRouters: { "2.0": "0x66a9893cc07d91d95644aedd05d03f95e1dba8af" },
33
+ wrappedNative: "0xc02aaA39b223fe8d0a0e5c4f27ead9083c756cc2".toLowerCase(),
34
+ ammProtocols: ["V2", "V3", "V4"],
35
+ },
36
+ base: {
37
+ chainId: 8453,
38
+ universalRouters: { "2.0": "0x6ff5693b99212da76ad316178a184ab56d299b43" },
39
+ wrappedNative: "0x4200000000000000000000000000000000000006",
40
+ ammProtocols: ["V2", "V3", "V4"],
41
+ },
42
+ robinhood: {
43
+ chainId: 4663,
44
+ universalRouters: { "2.0": "0x8876789976decbfcbbbe364623c63652db8c0904" },
45
+ wrappedNative: "0x0bd7d308f8e1639fab988df18a8011f41eacad73",
46
+ // Robinhood's official deployment exposes the V3 stack. Asking the Trading
47
+ // API for undeployed V2/V4 routes can make an otherwise valid V3-only pair
48
+ // appear unavailable, so keep the protocol set chain-specific.
49
+ ammProtocols: ["V3"],
50
+ v3DirectFallback: {
51
+ quoterV2: "0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7",
52
+ swapRouter02: "0xcaf681a66d020601342297493863e78c959e5cb2",
53
+ feeTiers: [100, 500, 3000, 10000],
54
+ },
55
+ },
31
56
  };
57
+ // Compatibility export for callers/tests. New execution code resolves a router
58
+ // from UNISWAP_EXECUTION_PROFILES and its explicitly selected version.
59
+ const UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK = Object.fromEntries(
60
+ Object.entries(UNISWAP_EXECUTION_PROFILES).map(([network, profile]) => [
61
+ network,
62
+ profile.universalRouters["2.0"],
63
+ ])
64
+ );
65
+ const UNISWAP_WRAPPED_NATIVE_BY_NETWORK = Object.fromEntries(
66
+ Object.entries(UNISWAP_EXECUTION_PROFILES).map(([network, profile]) => [
67
+ network,
68
+ profile.wrappedNative,
69
+ ])
70
+ );
71
+ const WETH_DEPOSIT_SELECTOR = "0xd0e30db0";
72
+ const WETH_WITHDRAW_SELECTOR = "0x2e1a7d4d";
73
+ const UNISWAP_V3_QUOTER_INTERFACE = new Interface([
74
+ "function quoteExactInputSingle((address tokenIn,address tokenOut,uint256 amountIn,uint24 fee,uint160 sqrtPriceLimitX96) params) external returns (uint256 amountOut,uint160 sqrtPriceX96After,uint32 initializedTicksCrossed,uint256 gasEstimate)",
75
+ ]);
76
+ const UNISWAP_V3_SWAP_ROUTER_INTERFACE = new Interface([
77
+ "function exactInputSingle((address tokenIn,address tokenOut,uint24 fee,address recipient,uint256 amountIn,uint256 amountOutMinimum,uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)",
78
+ "function unwrapWETH9(uint256 amountMinimum,address recipient) payable",
79
+ "function multicall(bytes[] data) payable returns (bytes[] results)",
80
+ ]);
81
+ const UNISWAP_CLASSIC_ROUTINGS = new Set(["CLASSIC"]);
82
+ const UNISWAPX_ROUTINGS = new Set(["DUTCH_V2", "DUTCH_V3", "LIMIT_ORDER", "PRIORITY"]);
32
83
  const AAVE_RAY = 10n ** 27n;
33
84
  const LIDO_STETH_DECIMALS = 18;
34
85
  const LIDO_MIN_STETH_WITHDRAWAL_AMOUNT = 100n;
@@ -857,6 +908,115 @@ function assertUniswapSupportedNetwork(network) {
857
908
  return chainId;
858
909
  }
859
910
 
911
+ function getUniswapNetworkExecutionProfile(network) {
912
+ const profile = UNISWAP_EXECUTION_PROFILES[network];
913
+ if (!profile) {
914
+ assertUniswapSupportedNetwork(network);
915
+ }
916
+ return profile;
917
+ }
918
+
919
+ function resolveUniswapRouterExecutionProfile(network, routerVersion) {
920
+ const networkProfile = getUniswapNetworkExecutionProfile(network);
921
+ const version = String(routerVersion || "2.0").trim() || "2.0";
922
+ const router = networkProfile.universalRouters[version];
923
+ if (!router) {
924
+ throw createTaggedError(
925
+ `Uniswap Universal Router version ${version} is not approved for ${network}.`,
926
+ "uniswap_router_version_unsupported",
927
+ {
928
+ network,
929
+ chainId: networkProfile.chainId,
930
+ requestedVersion: version,
931
+ supportedVersions: Object.keys(networkProfile.universalRouters),
932
+ }
933
+ );
934
+ }
935
+ return { ...networkProfile, routerVersion: version, router };
936
+ }
937
+
938
+ function encodeUint256(value) {
939
+ return BigInt(value).toString(16).padStart(64, "0");
940
+ }
941
+
942
+ function buildDirectWrappedNativeOperation(networkProfile, swapRequest) {
943
+ const wrappedNative = networkProfile.wrappedNative;
944
+ if (isZeroAddress(swapRequest.tokenIn) && swapRequest.tokenOut === wrappedNative) {
945
+ return {
946
+ executionKind: "wrap",
947
+ routing: "WRAP",
948
+ transaction: { to: wrappedNative, data: WETH_DEPOSIT_SELECTOR, value: swapRequest.tokenInAmount },
949
+ };
950
+ }
951
+ if (swapRequest.tokenIn === wrappedNative && isZeroAddress(swapRequest.tokenOut)) {
952
+ return {
953
+ executionKind: "unwrap",
954
+ routing: "UNWRAP",
955
+ transaction: {
956
+ to: wrappedNative,
957
+ data: `${WETH_WITHDRAW_SELECTOR}${encodeUint256(swapRequest.tokenInAmount)}`,
958
+ value: 0n,
959
+ },
960
+ };
961
+ }
962
+ return null;
963
+ }
964
+
965
+ function buildUniswapV3DirectSwapTransaction({
966
+ networkProfile,
967
+ recipient,
968
+ swapRequest,
969
+ feeTier,
970
+ minimumTokenOutAmount,
971
+ }) {
972
+ const fallback = networkProfile.v3DirectFallback;
973
+ if (!fallback) {
974
+ throw new Error("Uniswap V3 direct fallback is not configured for this network.");
975
+ }
976
+ const nativeTokenIn = isZeroAddress(swapRequest.tokenIn);
977
+ const nativeTokenOut = isZeroAddress(swapRequest.tokenOut);
978
+ const effectiveTokenIn = nativeTokenIn ? networkProfile.wrappedNative : swapRequest.tokenIn;
979
+ const effectiveTokenOut = nativeTokenOut ? networkProfile.wrappedNative : swapRequest.tokenOut;
980
+ const exactInput = UNISWAP_V3_SWAP_ROUTER_INTERFACE.encodeFunctionData("exactInputSingle", [
981
+ {
982
+ tokenIn: effectiveTokenIn,
983
+ tokenOut: effectiveTokenOut,
984
+ fee: feeTier,
985
+ // For an ETH output the router must receive WETH first, then unwrap it in
986
+ // the same atomic multicall. All other paths transfer directly to the user.
987
+ recipient: nativeTokenOut ? fallback.swapRouter02 : recipient,
988
+ amountIn: swapRequest.tokenInAmount,
989
+ amountOutMinimum: minimumTokenOutAmount,
990
+ sqrtPriceLimitX96: 0,
991
+ },
992
+ ]);
993
+ const data = nativeTokenOut
994
+ ? UNISWAP_V3_SWAP_ROUTER_INTERFACE.encodeFunctionData("multicall(bytes[])", [
995
+ [
996
+ exactInput,
997
+ UNISWAP_V3_SWAP_ROUTER_INTERFACE.encodeFunctionData("unwrapWETH9", [
998
+ minimumTokenOutAmount,
999
+ recipient,
1000
+ ]),
1001
+ ],
1002
+ ])
1003
+ : exactInput;
1004
+ return {
1005
+ to: fallback.swapRouter02,
1006
+ data,
1007
+ value: nativeTokenIn ? swapRequest.tokenInAmount : 0n,
1008
+ };
1009
+ }
1010
+
1011
+ function isUniswapNoQuotesAvailableError(error) {
1012
+ return (
1013
+ error &&
1014
+ typeof error === "object" &&
1015
+ error.errorCode === "network_unavailable" &&
1016
+ /no quotes available/i.test(String(error.message || ""))
1017
+ );
1018
+ }
1019
+
860
1020
  function uniswapSlippagePercentFromBps(bps) {
861
1021
  const parsed = Number(bps);
862
1022
  if (!Number.isInteger(parsed) || parsed < 0 || parsed > 5000) {
@@ -3757,36 +3917,73 @@ export class WdkEvmWalletService {
3757
3917
  );
3758
3918
  }
3759
3919
 
3760
- const signature =
3761
- finalPlan.isNativeTokenIn || !finalPlan.permitData
3762
- ? null
3763
- : await this.#signUniswapPermit(account, finalPlan.permitData, runtimeConfig);
3764
-
3765
- const swapTx = await this.#fetchUniswapSwapCalldata({
3766
- runtimeConfig,
3767
- quoteResponse: finalPlan.quoteResponse,
3768
- permitData: finalPlan.permitData,
3769
- signature,
3770
- });
3920
+ const signature = finalPlan.permitData
3921
+ ? await this.#signUniswapPermit(account, finalPlan.permitData, runtimeConfig)
3922
+ : null;
3771
3923
 
3772
- const simulation = await this.#simulatePreparedTransaction({
3773
- runtimeConfig,
3774
- from: address,
3775
- tx: swapTx,
3776
- });
3777
- this.#assertSimulationSucceeded(simulation);
3924
+ let simulation;
3925
+ let swapTransaction = null;
3926
+ let result;
3927
+ if (finalPlan.isUniswapX) {
3928
+ const order = await this.#submitUniswapOrder({
3929
+ quoteResponse: finalPlan.quoteResponse,
3930
+ signature,
3931
+ nativeTokenIn: finalPlan.isNativeTokenIn,
3932
+ routing: finalPlan.routing,
3933
+ routerProfile: finalPlan.routerProfile,
3934
+ });
3935
+ simulation = {
3936
+ ok: true,
3937
+ skipped: true,
3938
+ reason: "uniswapx-order",
3939
+ message: "UniswapX validates and submits the signed order to its filler network.",
3940
+ details: null,
3941
+ };
3942
+ result = {
3943
+ orderId: order.orderId,
3944
+ orderStatus: order.orderStatus,
3945
+ approvalFee: approvalExecution.totalFee.toString(),
3946
+ tokenInAmount: finalPlan.tokenInAmount.toString(),
3947
+ tokenOutAmount: finalPlan.tokenOutAmount.toString(),
3948
+ ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
3949
+ ...(approvalExecution.resetAllowanceHash
3950
+ ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
3951
+ : {}),
3952
+ };
3953
+ } else {
3954
+ const swapTx =
3955
+ finalPlan.preparedTransaction ||
3956
+ (await this.#fetchUniswapSwapCalldata({
3957
+ runtimeConfig,
3958
+ routerProfile: finalPlan.routerProfile,
3959
+ quoteResponse: finalPlan.quoteResponse,
3960
+ permitData: finalPlan.permitData,
3961
+ signature,
3962
+ }));
3963
+ simulation = await this.#simulatePreparedTransaction({
3964
+ runtimeConfig,
3965
+ from: address,
3966
+ tx: swapTx,
3967
+ });
3968
+ this.#assertSimulationSucceeded(simulation);
3778
3969
 
3779
- const { hash } = await account.sendTransaction(swapTx);
3780
- const result = {
3781
- hash,
3782
- approvalFee: approvalExecution.totalFee.toString(),
3783
- tokenInAmount: finalPlan.tokenInAmount.toString(),
3784
- tokenOutAmount: finalPlan.tokenOutAmount.toString(),
3785
- ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
3786
- ...(approvalExecution.resetAllowanceHash
3787
- ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
3788
- : {}),
3789
- };
3970
+ const { hash } = await account.sendTransaction(swapTx);
3971
+ swapTransaction = {
3972
+ to: swapTx.to,
3973
+ value: swapTx.value.toString(),
3974
+ dataHash: sha256Hex(swapTx.data),
3975
+ };
3976
+ result = {
3977
+ hash,
3978
+ approvalFee: approvalExecution.totalFee.toString(),
3979
+ tokenInAmount: finalPlan.tokenInAmount.toString(),
3980
+ tokenOutAmount: finalPlan.tokenOutAmount.toString(),
3981
+ ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
3982
+ ...(approvalExecution.resetAllowanceHash
3983
+ ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
3984
+ : {}),
3985
+ };
3986
+ }
3790
3987
  return {
3791
3988
  ...(await this.#formatUniswapSwapResponse({
3792
3989
  runtimeConfig,
@@ -3802,7 +3999,7 @@ export class WdkEvmWalletService {
3802
3999
  },
3803
4000
  })),
3804
4001
  simulation,
3805
- swapTransaction: { to: swapTx.to, value: swapTx.value.toString(), dataHash: sha256Hex(swapTx.data) },
4002
+ swapTransaction,
3806
4003
  result,
3807
4004
  };
3808
4005
  } catch (error) {
@@ -3845,7 +4042,8 @@ export class WdkEvmWalletService {
3845
4042
  address,
3846
4043
  protocol: "uniswap",
3847
4044
  executionSupported: true,
3848
- routing: "CLASSIC",
4045
+ executionKind: plan.executionKind,
4046
+ routing: plan.routing,
3849
4047
  swapRequest: {
3850
4048
  tokenIn: swapRequest.tokenIn,
3851
4049
  tokenOut: swapRequest.tokenOut,
@@ -3854,6 +4052,7 @@ export class WdkEvmWalletService {
3854
4052
  tokenInMetadata,
3855
4053
  tokenOutMetadata,
3856
4054
  inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
4055
+ outputAmount: plan.tokenOutAmount.toString(),
3857
4056
  outputAmountFormatted: formatUnits(plan.tokenOutAmount, tokenOutMetadata.decimals),
3858
4057
  quoteFingerprint: plan.quoteFingerprint,
3859
4058
  slippageBps: plan.slippageBps,
@@ -3871,7 +4070,19 @@ export class WdkEvmWalletService {
3871
4070
  readError: plan.allowanceReadError,
3872
4071
  },
3873
4072
  router: plan.router,
3874
- source: "uniswap-trading-api",
4073
+ swapTransaction: plan.preparedTransaction
4074
+ ? {
4075
+ to: plan.preparedTransaction.to,
4076
+ value: plan.preparedTransaction.value.toString(),
4077
+ dataHash: sha256Hex(plan.preparedTransaction.data),
4078
+ }
4079
+ : null,
4080
+ source:
4081
+ plan.executionKind === "wrap" || plan.executionKind === "unwrap"
4082
+ ? "canonical-wrapped-native"
4083
+ : plan.executionKind === "v3-direct"
4084
+ ? "uniswap-v3-direct-fallback"
4085
+ : "uniswap-trading-api",
3875
4086
  };
3876
4087
  }
3877
4088
 
@@ -5179,12 +5390,19 @@ export class WdkEvmWalletService {
5179
5390
  };
5180
5391
  }
5181
5392
 
5182
- async #uniswapTradingApiRequest(pathname, body) {
5393
+ async #uniswapTradingApiRequest(
5394
+ pathname,
5395
+ body,
5396
+ { erc20EthEnabled = false, routerVersion = this.config.uniswapRouterVersion, chainId = null } = {}
5397
+ ) {
5183
5398
  const headers = {
5184
5399
  "Content-Type": "application/json",
5185
5400
  Accept: "application/json",
5186
- "x-universal-router-version": this.config.uniswapRouterVersion,
5401
+ "x-universal-router-version": routerVersion,
5187
5402
  };
5403
+ if (erc20EthEnabled) {
5404
+ headers["x-erc20eth-enabled"] = "true";
5405
+ }
5188
5406
  if (this.config.uniswapViaGateway) {
5189
5407
  // The provider gateway holds the Uniswap key and injects x-api-key upstream;
5190
5408
  // we authenticate to it with the shared gateway bearer (same token used for
@@ -5193,6 +5411,13 @@ export class WdkEvmWalletService {
5193
5411
  if (token) {
5194
5412
  headers.Authorization = `Bearer ${token}`;
5195
5413
  }
5414
+ // The gateway selects its own configured version and checks this value for
5415
+ // equality. The chain id is an allow-listed routing hint, never an upstream
5416
+ // header passthrough.
5417
+ if (chainId !== null) {
5418
+ headers["x-agentlayer-chain-id"] = String(chainId);
5419
+ headers["x-agentlayer-uniswap-router-version"] = routerVersion;
5420
+ }
5196
5421
  } else {
5197
5422
  if (!this.config.uniswapApiKey) {
5198
5423
  throw createTaggedError(
@@ -5246,9 +5471,9 @@ export class WdkEvmWalletService {
5246
5471
  return payload;
5247
5472
  }
5248
5473
 
5249
- async #fetchUniswapQuote({ runtimeConfig, address, swapRequest }) {
5474
+ async #fetchUniswapQuote({ runtimeConfig, routerProfile, address, swapRequest }) {
5250
5475
  const chainId = UNISWAP_SUPPORTED_CHAIN_IDS[runtimeConfig.network];
5251
- const payload = await this.#uniswapTradingApiRequest("/quote", {
5476
+ const quoteRequest = {
5252
5477
  swapper: address,
5253
5478
  tokenIn: swapRequest.tokenIn,
5254
5479
  tokenOut: swapRequest.tokenOut,
@@ -5257,21 +5482,50 @@ export class WdkEvmWalletService {
5257
5482
  amount: swapRequest.tokenInAmount.toString(),
5258
5483
  type: "EXACT_INPUT",
5259
5484
  slippageTolerance: swapRequest.slippagePercent,
5260
- // The live Trading API rejects routingPreference:"CLASSIC"; restricting
5261
- // protocols to V2/V3/V4 is what excludes UniswapX and yields routing=CLASSIC.
5262
- protocols: ["V2", "V3", "V4"],
5263
- });
5485
+ };
5486
+ const requestOptions = {
5487
+ erc20EthEnabled: isZeroAddress(swapRequest.tokenIn),
5488
+ routerVersion: routerProfile.routerVersion,
5489
+ chainId,
5490
+ };
5491
+ const ammProtocols = routerProfile.ammProtocols;
5492
+ let payload;
5493
+ try {
5494
+ // Prefer the full route set. On a sub-minimum UniswapX request the API can
5495
+ // return No quotes available even when an AMM pool is liquid, so retry the
5496
+ // same exact swap through AMMs only before reporting a quote failure.
5497
+ payload = await this.#uniswapTradingApiRequest(
5498
+ "/quote",
5499
+ {
5500
+ ...quoteRequest,
5501
+ protocols: [
5502
+ ...ammProtocols,
5503
+ runtimeConfig.network === "ethereum" ? "UNISWAPX_V2" : "UNISWAPX_V3",
5504
+ ],
5505
+ },
5506
+ requestOptions
5507
+ );
5508
+ } catch (error) {
5509
+ if (!isUniswapNoQuotesAvailableError(error)) {
5510
+ throw error;
5511
+ }
5512
+ payload = await this.#uniswapTradingApiRequest(
5513
+ "/quote",
5514
+ { ...quoteRequest, protocols: ammProtocols },
5515
+ requestOptions
5516
+ );
5517
+ }
5264
5518
  const routing = String(payload.routing || "").toUpperCase();
5265
- if (routing !== "CLASSIC") {
5519
+ if (!UNISWAP_CLASSIC_ROUTINGS.has(routing) && !UNISWAPX_ROUTINGS.has(routing)) {
5266
5520
  throw createTaggedError(
5267
- `Uniswap returned unsupported routing '${routing}'. Only CLASSIC is enabled in this runtime.`,
5521
+ `Uniswap returned unsupported routing '${routing}'.`,
5268
5522
  "uniswap_unsupported_route",
5269
5523
  { provider: "uniswap", routing }
5270
5524
  );
5271
5525
  }
5272
5526
  if (!payload.quote || typeof payload.quote !== "object" || !payload.quote.output) {
5273
5527
  throw createTaggedError(
5274
- "Uniswap quote response is missing CLASSIC quote/output fields.",
5528
+ "Uniswap quote response is missing quote/output fields.",
5275
5529
  "network_unavailable",
5276
5530
  { provider: "uniswap" }
5277
5531
  );
@@ -5279,8 +5533,195 @@ export class WdkEvmWalletService {
5279
5533
  return payload;
5280
5534
  }
5281
5535
 
5536
+ async #quoteRobinhoodV3Direct({ runtimeConfig, networkProfile, swapRequest }) {
5537
+ const fallback = networkProfile.v3DirectFallback;
5538
+ if (!fallback) {
5539
+ return null;
5540
+ }
5541
+ const tokenIn = isZeroAddress(swapRequest.tokenIn)
5542
+ ? networkProfile.wrappedNative
5543
+ : swapRequest.tokenIn;
5544
+ const tokenOut = isZeroAddress(swapRequest.tokenOut)
5545
+ ? networkProfile.wrappedNative
5546
+ : swapRequest.tokenOut;
5547
+ let best = null;
5548
+ for (const feeTier of fallback.feeTiers) {
5549
+ const data = UNISWAP_V3_QUOTER_INTERFACE.encodeFunctionData("quoteExactInputSingle", [
5550
+ {
5551
+ tokenIn,
5552
+ tokenOut,
5553
+ amountIn: swapRequest.tokenInAmount,
5554
+ fee: feeTier,
5555
+ sqrtPriceLimitX96: 0,
5556
+ },
5557
+ ]);
5558
+ try {
5559
+ const raw = await rpcRequest(runtimeConfig.providerUrl, "eth_call", [
5560
+ { to: fallback.quoterV2, data },
5561
+ "latest",
5562
+ ]);
5563
+ const decoded = UNISWAP_V3_QUOTER_INTERFACE.decodeFunctionResult(
5564
+ "quoteExactInputSingle",
5565
+ raw
5566
+ );
5567
+ const amountOut = BigInt(decoded[0]);
5568
+ if (amountOut > 0n && (!best || amountOut > best.amountOut)) {
5569
+ best = { feeTier, amountOut, gasEstimate: BigInt(decoded[3]) };
5570
+ }
5571
+ } catch {
5572
+ // A missing pool and a failed Quoter call are equivalent for this
5573
+ // constrained direct route. Try every reviewed fee tier before failing.
5574
+ }
5575
+ }
5576
+ return best;
5577
+ }
5578
+
5579
+ async #buildRobinhoodV3DirectFallbackPlan({ account, runtimeConfig, address, swapRequest }) {
5580
+ const networkProfile = getUniswapNetworkExecutionProfile(runtimeConfig.network);
5581
+ const quoted = await this.#quoteRobinhoodV3Direct({
5582
+ runtimeConfig,
5583
+ networkProfile,
5584
+ swapRequest,
5585
+ });
5586
+ if (!quoted) {
5587
+ return null;
5588
+ }
5589
+ const slippageBps = Math.round(swapRequest.slippagePercent * 100);
5590
+ const minimumTokenOutAmount =
5591
+ quoted.amountOut - (quoted.amountOut * BigInt(slippageBps)) / 10000n;
5592
+ const nativeTokenIn = isZeroAddress(swapRequest.tokenIn);
5593
+ const spender = nativeTokenIn ? null : networkProfile.v3DirectFallback.swapRouter02;
5594
+ const allowanceState = nativeTokenIn
5595
+ ? { currentAllowance: swapRequest.tokenInAmount, error: null }
5596
+ : await this.#getSwapAllowanceState({
5597
+ account,
5598
+ tokenAddress: swapRequest.tokenIn,
5599
+ spender,
5600
+ });
5601
+ const approval = nativeTokenIn
5602
+ ? { required: false, estimatedFee: 0n, steps: [] }
5603
+ : await this.#buildSwapApprovalPlan({
5604
+ account,
5605
+ runtimeConfig,
5606
+ tokenAddress: swapRequest.tokenIn,
5607
+ spender,
5608
+ requiredAmount: swapRequest.tokenInAmount,
5609
+ currentAllowance: allowanceState.currentAllowance,
5610
+ });
5611
+ const preparedTransaction = buildUniswapV3DirectSwapTransaction({
5612
+ networkProfile,
5613
+ recipient: address,
5614
+ swapRequest,
5615
+ feeTier: quoted.feeTier,
5616
+ minimumTokenOutAmount,
5617
+ });
5618
+ const quoteFingerprint = sha256Hex(
5619
+ JSON.stringify({
5620
+ chainId: runtimeConfig.chainId,
5621
+ network: runtimeConfig.network,
5622
+ from: address.toLowerCase(),
5623
+ router: networkProfile.v3DirectFallback.swapRouter02,
5624
+ spender: spender ? spender.toLowerCase() : null,
5625
+ tokenIn: swapRequest.tokenIn.toLowerCase(),
5626
+ tokenOut: swapRequest.tokenOut.toLowerCase(),
5627
+ tokenInAmount: swapRequest.tokenInAmount.toString(),
5628
+ feeTier: quoted.feeTier,
5629
+ slippageBps,
5630
+ executionKind: "v3-direct",
5631
+ })
5632
+ );
5633
+ return {
5634
+ quoteResponse: null,
5635
+ permitData: null,
5636
+ isNativeTokenIn: nativeTokenIn,
5637
+ spender,
5638
+ currentAllowance: allowanceState.currentAllowance,
5639
+ allowanceReadError: allowanceState.error,
5640
+ approval,
5641
+ tokenInAmount: swapRequest.tokenInAmount,
5642
+ tokenOutAmount: quoted.amountOut,
5643
+ minimumTokenOutAmount,
5644
+ slippageBps,
5645
+ routing: "CLASSIC",
5646
+ executionKind: "v3-direct",
5647
+ isUniswapX: false,
5648
+ quoteFingerprint,
5649
+ // QuoterV2 returns a gas-unit estimate, not a wei-denominated fee.
5650
+ // Keep the fee field empty rather than mislabeling units in the preview.
5651
+ gasFee: null,
5652
+ gasFeeUSD: null,
5653
+ router: networkProfile.v3DirectFallback.swapRouter02,
5654
+ routerProfile: null,
5655
+ preparedTransaction,
5656
+ v3FeeTier: quoted.feeTier,
5657
+ };
5658
+ }
5659
+
5282
5660
  async #buildUniswapSwapPlan({ account, runtimeConfig, address, swapRequest }) {
5283
- const quoteResponse = await this.#fetchUniswapQuote({ runtimeConfig, address, swapRequest });
5661
+ const networkProfile = getUniswapNetworkExecutionProfile(runtimeConfig.network);
5662
+ const directOperation = buildDirectWrappedNativeOperation(networkProfile, swapRequest);
5663
+ const slippageBps = Math.round(swapRequest.slippagePercent * 100);
5664
+ if (directOperation) {
5665
+ const quoteFingerprint = sha256Hex(
5666
+ JSON.stringify({
5667
+ chainId: runtimeConfig.chainId,
5668
+ network: runtimeConfig.network,
5669
+ from: address.toLowerCase(),
5670
+ executionKind: directOperation.executionKind,
5671
+ wrappedNative: networkProfile.wrappedNative,
5672
+ tokenIn: swapRequest.tokenIn,
5673
+ tokenOut: swapRequest.tokenOut,
5674
+ tokenInAmount: swapRequest.tokenInAmount.toString(),
5675
+ })
5676
+ );
5677
+ return {
5678
+ quoteResponse: null,
5679
+ permitData: null,
5680
+ isNativeTokenIn: isZeroAddress(swapRequest.tokenIn),
5681
+ spender: null,
5682
+ currentAllowance: 0n,
5683
+ allowanceReadError: null,
5684
+ approval: { required: false, estimatedFee: 0n, steps: [] },
5685
+ tokenInAmount: swapRequest.tokenInAmount,
5686
+ tokenOutAmount: swapRequest.tokenInAmount,
5687
+ minimumTokenOutAmount: swapRequest.tokenInAmount,
5688
+ slippageBps,
5689
+ routing: directOperation.routing,
5690
+ executionKind: directOperation.executionKind,
5691
+ isUniswapX: false,
5692
+ quoteFingerprint,
5693
+ gasFee: null,
5694
+ gasFeeUSD: null,
5695
+ router: networkProfile.wrappedNative,
5696
+ routerProfile: null,
5697
+ preparedTransaction: directOperation.transaction,
5698
+ };
5699
+ }
5700
+
5701
+ const routerProfile = this.#resolveUniswapRouterProfile(runtimeConfig);
5702
+ let quoteResponse;
5703
+ try {
5704
+ quoteResponse = await this.#fetchUniswapQuote({
5705
+ runtimeConfig,
5706
+ routerProfile,
5707
+ address,
5708
+ swapRequest,
5709
+ });
5710
+ } catch (error) {
5711
+ if (runtimeConfig.network !== "robinhood" || !isUniswapNoQuotesAvailableError(error)) {
5712
+ throw error;
5713
+ }
5714
+ const fallbackPlan = await this.#buildRobinhoodV3DirectFallbackPlan({
5715
+ account,
5716
+ runtimeConfig,
5717
+ address,
5718
+ swapRequest,
5719
+ });
5720
+ if (fallbackPlan) {
5721
+ return fallbackPlan;
5722
+ }
5723
+ throw error;
5724
+ }
5284
5725
  const permitData = quoteResponse.permitData ?? null;
5285
5726
  const isNativeTokenIn = isZeroAddress(swapRequest.tokenIn);
5286
5727
  const spender = isNativeTokenIn ? null : PERMIT2_ADDRESS;
@@ -5302,16 +5743,17 @@ export class WdkEvmWalletService {
5302
5743
  requiredAmount: swapRequest.tokenInAmount,
5303
5744
  currentAllowance,
5304
5745
  });
5305
- const tokenOutAmount = BigInt(String(quoteResponse.quote.output.amount || "0"));
5306
- const slippageBps = Math.round(swapRequest.slippagePercent * 100);
5746
+ const quoteOutput = quoteResponse.quote.output;
5747
+ const tokenOutAmount = BigInt(String(quoteOutput.amount || "0"));
5307
5748
  // The Trading API returns the post-slippage floor directly; fall back to a
5308
5749
  // local computation only if the field is absent.
5309
5750
  const minimumTokenOutAmount =
5310
- quoteResponse.quote.output.minimumAmount !== undefined &&
5311
- quoteResponse.quote.output.minimumAmount !== null
5312
- ? BigInt(String(quoteResponse.quote.output.minimumAmount))
5751
+ quoteOutput.minimumAmount !== undefined && quoteOutput.minimumAmount !== null
5752
+ ? BigInt(String(quoteOutput.minimumAmount))
5313
5753
  : tokenOutAmount - (tokenOutAmount * BigInt(slippageBps)) / 10000n;
5314
- const router = UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK[runtimeConfig.network];
5754
+ const routing = String(quoteResponse.routing || "").toUpperCase();
5755
+ const isUniswapX = UNISWAPX_ROUTINGS.has(routing);
5756
+ const router = isUniswapX ? null : routerProfile.router;
5315
5757
  // Bind only the stable swap *intent* (who/what/how-much/slippage/route), never
5316
5758
  // the live quoted output: the Trading API re-prices every block, so including
5317
5759
  // tokenOutAmount here made execute's re-quote fingerprint differ from preview's
@@ -5323,13 +5765,15 @@ export class WdkEvmWalletService {
5323
5765
  chainId: runtimeConfig.chainId,
5324
5766
  network: runtimeConfig.network,
5325
5767
  from: address.toLowerCase(),
5326
- router: router.toLowerCase(),
5768
+ router: router ? router.toLowerCase() : null,
5327
5769
  spender: spender ? spender.toLowerCase() : null,
5328
5770
  tokenIn: swapRequest.tokenIn.toLowerCase(),
5329
5771
  tokenOut: swapRequest.tokenOut.toLowerCase(),
5330
5772
  tokenInAmount: swapRequest.tokenInAmount.toString(),
5331
5773
  slippageBps,
5332
- routing: "CLASSIC",
5774
+ routing,
5775
+ executionKind: isUniswapX ? "uniswapx" : "classic",
5776
+ routerVersion: routerProfile.routerVersion,
5333
5777
  })
5334
5778
  );
5335
5779
  return {
@@ -5344,13 +5788,25 @@ export class WdkEvmWalletService {
5344
5788
  tokenOutAmount,
5345
5789
  minimumTokenOutAmount,
5346
5790
  slippageBps,
5791
+ routing,
5792
+ executionKind: isUniswapX ? "uniswapx" : "classic",
5793
+ isUniswapX,
5347
5794
  quoteFingerprint,
5348
5795
  gasFee: quoteResponse.quote.gasFee ?? null,
5349
5796
  gasFeeUSD: quoteResponse.quote.gasFeeUSD ?? null,
5350
5797
  router,
5798
+ routerProfile,
5799
+ preparedTransaction: null,
5351
5800
  };
5352
5801
  }
5353
5802
 
5803
+ #resolveUniswapRouterProfile(runtimeConfig) {
5804
+ const configuredVersion =
5805
+ this.config.uniswapRouterVersionsByNetwork?.[runtimeConfig.network] ||
5806
+ this.config.uniswapRouterVersion;
5807
+ return resolveUniswapRouterExecutionProfile(runtimeConfig.network, configuredVersion);
5808
+ }
5809
+
5354
5810
  async #signUniswapPermit(account, permitData, runtimeConfig) {
5355
5811
  const typed = normalizeUniswapPermitData(permitData, runtimeConfig);
5356
5812
  return account.signTypedData({
@@ -5360,7 +5816,13 @@ export class WdkEvmWalletService {
5360
5816
  });
5361
5817
  }
5362
5818
 
5363
- async #fetchUniswapSwapCalldata({ runtimeConfig, quoteResponse, permitData, signature }) {
5819
+ async #fetchUniswapSwapCalldata({
5820
+ runtimeConfig,
5821
+ routerProfile,
5822
+ quoteResponse,
5823
+ permitData,
5824
+ signature,
5825
+ }) {
5364
5826
  const { permitData: _permitData, permitTransaction: _permitTransaction, ...cleanQuote } =
5365
5827
  quoteResponse;
5366
5828
  const body = { ...cleanQuote };
@@ -5370,10 +5832,13 @@ export class WdkEvmWalletService {
5370
5832
  body.signature = signature;
5371
5833
  body.permitData = permitData;
5372
5834
  }
5373
- const payload = await this.#uniswapTradingApiRequest("/swap", body);
5835
+ const payload = await this.#uniswapTradingApiRequest("/swap", body, {
5836
+ routerVersion: routerProfile.routerVersion,
5837
+ chainId: runtimeConfig.chainId,
5838
+ });
5374
5839
  const swap = payload.swap || {};
5375
5840
  const to = normalizeAddress(String(swap.to || ""), "swap.to");
5376
- const expectedRouter = UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK[runtimeConfig.network];
5841
+ const expectedRouter = routerProfile.router;
5377
5842
  if (to.toLowerCase() !== expectedRouter) {
5378
5843
  throw createTaggedError(
5379
5844
  "Uniswap /swap returned an unexpected target contract.",
@@ -5382,6 +5847,7 @@ export class WdkEvmWalletService {
5382
5847
  );
5383
5848
  }
5384
5849
  const data = assertNonEmptyString(String(swap.data || ""), "swap.data");
5850
+ const value = parseHexOrDecimalBigInt(swap.value || "0", "swap.value");
5385
5851
  if (data === "0x") {
5386
5852
  throw createTaggedError(
5387
5853
  "Uniswap /swap returned empty calldata. The quote likely expired; generate a new preview.",
@@ -5392,7 +5858,38 @@ export class WdkEvmWalletService {
5392
5858
  return {
5393
5859
  to,
5394
5860
  data,
5395
- value: parseHexOrDecimalBigInt(swap.value || "0", "swap.value"),
5861
+ value,
5862
+ };
5863
+ }
5864
+
5865
+ async #submitUniswapOrder({ quoteResponse, signature, nativeTokenIn, routing, routerProfile }) {
5866
+ if (!signature) {
5867
+ throw createTaggedError(
5868
+ "UniswapX order quote is missing required signature data.",
5869
+ "uniswap_order_signature_missing",
5870
+ { provider: "uniswap" }
5871
+ );
5872
+ }
5873
+ const payload = await this.#uniswapTradingApiRequest(
5874
+ "/order",
5875
+ { quote: quoteResponse.quote, routing, signature },
5876
+ {
5877
+ erc20EthEnabled: nativeTokenIn,
5878
+ routerVersion: routerProfile.routerVersion,
5879
+ chainId: routerProfile.chainId,
5880
+ }
5881
+ );
5882
+ const orderId = String(payload.orderId || payload.order?.orderId || "").trim();
5883
+ if (!orderId) {
5884
+ throw createTaggedError(
5885
+ "Uniswap /order response is missing orderId.",
5886
+ "network_unavailable",
5887
+ { provider: "uniswap" }
5888
+ );
5889
+ }
5890
+ return {
5891
+ orderId,
5892
+ orderStatus: String(payload.orderStatus || payload.order?.orderStatus || "open"),
5396
5893
  };
5397
5894
  }
5398
5895
 
@@ -7562,9 +8059,13 @@ export class WdkEvmWalletService {
7562
8059
  export const __testables = {
7563
8060
  PERMIT2_ADDRESS,
7564
8061
  UNISWAP_SUPPORTED_CHAIN_IDS,
8062
+ UNISWAP_EXECUTION_PROFILES,
7565
8063
  UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK,
8064
+ UNISWAP_WRAPPED_NATIVE_BY_NETWORK,
7566
8065
  normalizeUniswapTokenAddress,
7567
8066
  assertUniswapSupportedNetwork,
8067
+ resolveUniswapRouterExecutionProfile,
8068
+ buildDirectWrappedNativeOperation,
7568
8069
  assertValidNetwork,
7569
8070
  uniswapSlippagePercentFromBps,
7570
8071
  normalizeUniswapPermitData,