@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.
- package/.openclaw/extensions/agent-wallet/dist/index.js +2 -2
- package/.openclaw/extensions/agent-wallet/index.ts +2 -2
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +6 -4
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +23 -8
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/.env.example +3 -0
- package/wdk-evm-wallet/package.json +1 -1
- package/wdk-evm-wallet/src/config.js +32 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +561 -60
|
@@ -1963,7 +1963,7 @@ const evmToolDefinitions = [
|
|
|
1963
1963
|
},
|
|
1964
1964
|
{
|
|
1965
1965
|
name: "get_uniswap_swap_quote",
|
|
1966
|
-
description: "Get a read-only Uniswap
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
5
|
+
"version": "0.1.84",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"agentlayer_autonomous_approve",
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.84
|
|
@@ -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
|
|
2178
|
-
"on ethereum, base, or robinhood.
|
|
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
|
|
2219
|
-
"
|
|
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":
|
|
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.
|
|
5
|
+
"version": "0.1.84",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -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
|
-
-
|
|
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.
|
|
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"
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -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
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
3762
|
-
|
|
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
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
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
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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":
|
|
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
|
|
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
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
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
|
|
5519
|
+
if (!UNISWAP_CLASSIC_ROUTINGS.has(routing) && !UNISWAPX_ROUTINGS.has(routing)) {
|
|
5266
5520
|
throw createTaggedError(
|
|
5267
|
-
`Uniswap returned unsupported routing '${routing}'
|
|
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
|
|
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
|
|
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
|
|
5306
|
-
const
|
|
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
|
-
|
|
5311
|
-
|
|
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
|
|
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
|
|
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({
|
|
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 =
|
|
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
|
|
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,
|