@agentlayer.tech/wallet 0.1.76 → 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.
Files changed (29) hide show
  1. package/.openclaw/extensions/agent-wallet/README.md +4 -4
  2. package/.openclaw/extensions/agent-wallet/dist/index.js +57 -19
  3. package/.openclaw/extensions/agent-wallet/index.ts +57 -19
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
  5. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  6. package/CHANGELOG.md +19 -0
  7. package/VERSION +1 -1
  8. package/agent-wallet/agent_wallet/__init__.py +1 -1
  9. package/agent-wallet/agent_wallet/autonomous_permissions.py +2 -2
  10. package/agent-wallet/agent_wallet/autonomous_policy.py +1 -1
  11. package/agent-wallet/agent_wallet/config.py +3 -3
  12. package/agent-wallet/agent_wallet/openclaw_adapter.py +28 -26
  13. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +2 -2
  14. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +25 -10
  15. package/agent-wallet/openclaw.plugin.json +1 -1
  16. package/agent-wallet/pyproject.toml +1 -1
  17. package/agent-wallet/skills/wallet-operator/SKILL.md +4 -1
  18. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  19. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  20. package/codex/plugins/agent-wallet/server.py +13 -10
  21. package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
  22. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  23. package/package.json +1 -1
  24. package/wdk-btc-wallet/package.json +1 -1
  25. package/wdk-evm-wallet/.env.example +6 -2
  26. package/wdk-evm-wallet/package.json +5 -2
  27. package/wdk-evm-wallet/src/config.js +48 -2
  28. package/wdk-evm-wallet/src/network_state.js +5 -2
  29. package/wdk-evm-wallet/src/wdk_evm_wallet.js +570 -63
@@ -22,12 +22,64 @@ const DEFAULT_SWAP_SLIPPAGE_BPS = 100;
22
22
  const DEFAULT_LIFI_SLIPPAGE = 0.005;
23
23
  const ALWAYS_DENIED_LIFI_BRIDGES = ["mayan"];
24
24
  const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
25
- const UNISWAP_SUPPORTED_CHAIN_IDS = { ethereum: 1, base: 8453 };
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",
25
+ const UNISWAP_SUPPORTED_CHAIN_IDS = { ethereum: 1, base: 8453, robinhood: 4663 };
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
+ },
30
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"]);
31
83
  const AAVE_RAY = 10n ** 27n;
32
84
  const LIDO_STETH_DECIMALS = 18;
33
85
  const LIDO_MIN_STETH_WITHDRAWAL_AMOUNT = 100n;
@@ -502,10 +554,13 @@ function assertValidNetwork(network, fieldName = "network") {
502
554
  eth: "ethereum",
503
555
  "base-mainnet": "base",
504
556
  base_sepolia: "base-sepolia",
557
+ "robinhood-mainnet": "robinhood",
505
558
  };
506
559
  const effective = aliases[normalized] || normalized;
507
- if (!["ethereum", "sepolia", "base", "base-sepolia"].includes(effective)) {
508
- throw new Error(`${fieldName} must be one of: ethereum, sepolia, base, base-sepolia.`);
560
+ if (!["ethereum", "sepolia", "base", "base-sepolia", "robinhood"].includes(effective)) {
561
+ throw new Error(
562
+ `${fieldName} must be one of: ethereum, sepolia, base, base-sepolia, robinhood.`
563
+ );
509
564
  }
510
565
  return effective;
511
566
  }
@@ -847,12 +902,121 @@ function assertUniswapSupportedNetwork(network) {
847
902
  const chainId = UNISWAP_SUPPORTED_CHAIN_IDS[network];
848
903
  if (!chainId) {
849
904
  throw new Error(
850
- "Uniswap Trading API swaps are currently supported only on ethereum and base mainnet."
905
+ "Uniswap Trading API swaps are currently supported only on ethereum, base, and robinhood mainnet."
851
906
  );
852
907
  }
853
908
  return chainId;
854
909
  }
855
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
+
856
1020
  function uniswapSlippagePercentFromBps(bps) {
857
1021
  const parsed = Number(bps);
858
1022
  if (!Number.isInteger(parsed) || parsed < 0 || parsed > 5000) {
@@ -3753,36 +3917,73 @@ export class WdkEvmWalletService {
3753
3917
  );
3754
3918
  }
3755
3919
 
3756
- const signature =
3757
- finalPlan.isNativeTokenIn || !finalPlan.permitData
3758
- ? null
3759
- : await this.#signUniswapPermit(account, finalPlan.permitData, runtimeConfig);
3760
-
3761
- const swapTx = await this.#fetchUniswapSwapCalldata({
3762
- runtimeConfig,
3763
- quoteResponse: finalPlan.quoteResponse,
3764
- permitData: finalPlan.permitData,
3765
- signature,
3766
- });
3920
+ const signature = finalPlan.permitData
3921
+ ? await this.#signUniswapPermit(account, finalPlan.permitData, runtimeConfig)
3922
+ : null;
3767
3923
 
3768
- const simulation = await this.#simulatePreparedTransaction({
3769
- runtimeConfig,
3770
- from: address,
3771
- tx: swapTx,
3772
- });
3773
- 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);
3774
3969
 
3775
- const { hash } = await account.sendTransaction(swapTx);
3776
- const result = {
3777
- hash,
3778
- approvalFee: approvalExecution.totalFee.toString(),
3779
- tokenInAmount: finalPlan.tokenInAmount.toString(),
3780
- tokenOutAmount: finalPlan.tokenOutAmount.toString(),
3781
- ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
3782
- ...(approvalExecution.resetAllowanceHash
3783
- ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
3784
- : {}),
3785
- };
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
+ }
3786
3987
  return {
3787
3988
  ...(await this.#formatUniswapSwapResponse({
3788
3989
  runtimeConfig,
@@ -3798,7 +3999,7 @@ export class WdkEvmWalletService {
3798
3999
  },
3799
4000
  })),
3800
4001
  simulation,
3801
- swapTransaction: { to: swapTx.to, value: swapTx.value.toString(), dataHash: sha256Hex(swapTx.data) },
4002
+ swapTransaction,
3802
4003
  result,
3803
4004
  };
3804
4005
  } catch (error) {
@@ -3841,7 +4042,8 @@ export class WdkEvmWalletService {
3841
4042
  address,
3842
4043
  protocol: "uniswap",
3843
4044
  executionSupported: true,
3844
- routing: "CLASSIC",
4045
+ executionKind: plan.executionKind,
4046
+ routing: plan.routing,
3845
4047
  swapRequest: {
3846
4048
  tokenIn: swapRequest.tokenIn,
3847
4049
  tokenOut: swapRequest.tokenOut,
@@ -3850,6 +4052,7 @@ export class WdkEvmWalletService {
3850
4052
  tokenInMetadata,
3851
4053
  tokenOutMetadata,
3852
4054
  inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
4055
+ outputAmount: plan.tokenOutAmount.toString(),
3853
4056
  outputAmountFormatted: formatUnits(plan.tokenOutAmount, tokenOutMetadata.decimals),
3854
4057
  quoteFingerprint: plan.quoteFingerprint,
3855
4058
  slippageBps: plan.slippageBps,
@@ -3867,7 +4070,19 @@ export class WdkEvmWalletService {
3867
4070
  readError: plan.allowanceReadError,
3868
4071
  },
3869
4072
  router: plan.router,
3870
- 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",
3871
4086
  };
3872
4087
  }
3873
4088
 
@@ -5175,12 +5390,19 @@ export class WdkEvmWalletService {
5175
5390
  };
5176
5391
  }
5177
5392
 
5178
- async #uniswapTradingApiRequest(pathname, body) {
5393
+ async #uniswapTradingApiRequest(
5394
+ pathname,
5395
+ body,
5396
+ { erc20EthEnabled = false, routerVersion = this.config.uniswapRouterVersion, chainId = null } = {}
5397
+ ) {
5179
5398
  const headers = {
5180
5399
  "Content-Type": "application/json",
5181
5400
  Accept: "application/json",
5182
- "x-universal-router-version": this.config.uniswapRouterVersion,
5401
+ "x-universal-router-version": routerVersion,
5183
5402
  };
5403
+ if (erc20EthEnabled) {
5404
+ headers["x-erc20eth-enabled"] = "true";
5405
+ }
5184
5406
  if (this.config.uniswapViaGateway) {
5185
5407
  // The provider gateway holds the Uniswap key and injects x-api-key upstream;
5186
5408
  // we authenticate to it with the shared gateway bearer (same token used for
@@ -5189,6 +5411,13 @@ export class WdkEvmWalletService {
5189
5411
  if (token) {
5190
5412
  headers.Authorization = `Bearer ${token}`;
5191
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
+ }
5192
5421
  } else {
5193
5422
  if (!this.config.uniswapApiKey) {
5194
5423
  throw createTaggedError(
@@ -5242,9 +5471,9 @@ export class WdkEvmWalletService {
5242
5471
  return payload;
5243
5472
  }
5244
5473
 
5245
- async #fetchUniswapQuote({ runtimeConfig, address, swapRequest }) {
5474
+ async #fetchUniswapQuote({ runtimeConfig, routerProfile, address, swapRequest }) {
5246
5475
  const chainId = UNISWAP_SUPPORTED_CHAIN_IDS[runtimeConfig.network];
5247
- const payload = await this.#uniswapTradingApiRequest("/quote", {
5476
+ const quoteRequest = {
5248
5477
  swapper: address,
5249
5478
  tokenIn: swapRequest.tokenIn,
5250
5479
  tokenOut: swapRequest.tokenOut,
@@ -5253,21 +5482,50 @@ export class WdkEvmWalletService {
5253
5482
  amount: swapRequest.tokenInAmount.toString(),
5254
5483
  type: "EXACT_INPUT",
5255
5484
  slippageTolerance: swapRequest.slippagePercent,
5256
- // The live Trading API rejects routingPreference:"CLASSIC"; restricting
5257
- // protocols to V2/V3/V4 is what excludes UniswapX and yields routing=CLASSIC.
5258
- protocols: ["V2", "V3", "V4"],
5259
- });
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
+ }
5260
5518
  const routing = String(payload.routing || "").toUpperCase();
5261
- if (routing !== "CLASSIC") {
5519
+ if (!UNISWAP_CLASSIC_ROUTINGS.has(routing) && !UNISWAPX_ROUTINGS.has(routing)) {
5262
5520
  throw createTaggedError(
5263
- `Uniswap returned unsupported routing '${routing}'. Only CLASSIC is enabled in this runtime.`,
5521
+ `Uniswap returned unsupported routing '${routing}'.`,
5264
5522
  "uniswap_unsupported_route",
5265
5523
  { provider: "uniswap", routing }
5266
5524
  );
5267
5525
  }
5268
5526
  if (!payload.quote || typeof payload.quote !== "object" || !payload.quote.output) {
5269
5527
  throw createTaggedError(
5270
- "Uniswap quote response is missing CLASSIC quote/output fields.",
5528
+ "Uniswap quote response is missing quote/output fields.",
5271
5529
  "network_unavailable",
5272
5530
  { provider: "uniswap" }
5273
5531
  );
@@ -5275,8 +5533,195 @@ export class WdkEvmWalletService {
5275
5533
  return payload;
5276
5534
  }
5277
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
+
5278
5660
  async #buildUniswapSwapPlan({ account, runtimeConfig, address, swapRequest }) {
5279
- 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
+ }
5280
5725
  const permitData = quoteResponse.permitData ?? null;
5281
5726
  const isNativeTokenIn = isZeroAddress(swapRequest.tokenIn);
5282
5727
  const spender = isNativeTokenIn ? null : PERMIT2_ADDRESS;
@@ -5298,16 +5743,17 @@ export class WdkEvmWalletService {
5298
5743
  requiredAmount: swapRequest.tokenInAmount,
5299
5744
  currentAllowance,
5300
5745
  });
5301
- const tokenOutAmount = BigInt(String(quoteResponse.quote.output.amount || "0"));
5302
- const slippageBps = Math.round(swapRequest.slippagePercent * 100);
5746
+ const quoteOutput = quoteResponse.quote.output;
5747
+ const tokenOutAmount = BigInt(String(quoteOutput.amount || "0"));
5303
5748
  // The Trading API returns the post-slippage floor directly; fall back to a
5304
5749
  // local computation only if the field is absent.
5305
5750
  const minimumTokenOutAmount =
5306
- quoteResponse.quote.output.minimumAmount !== undefined &&
5307
- quoteResponse.quote.output.minimumAmount !== null
5308
- ? BigInt(String(quoteResponse.quote.output.minimumAmount))
5751
+ quoteOutput.minimumAmount !== undefined && quoteOutput.minimumAmount !== null
5752
+ ? BigInt(String(quoteOutput.minimumAmount))
5309
5753
  : tokenOutAmount - (tokenOutAmount * BigInt(slippageBps)) / 10000n;
5310
- 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;
5311
5757
  // Bind only the stable swap *intent* (who/what/how-much/slippage/route), never
5312
5758
  // the live quoted output: the Trading API re-prices every block, so including
5313
5759
  // tokenOutAmount here made execute's re-quote fingerprint differ from preview's
@@ -5319,13 +5765,15 @@ export class WdkEvmWalletService {
5319
5765
  chainId: runtimeConfig.chainId,
5320
5766
  network: runtimeConfig.network,
5321
5767
  from: address.toLowerCase(),
5322
- router: router.toLowerCase(),
5768
+ router: router ? router.toLowerCase() : null,
5323
5769
  spender: spender ? spender.toLowerCase() : null,
5324
5770
  tokenIn: swapRequest.tokenIn.toLowerCase(),
5325
5771
  tokenOut: swapRequest.tokenOut.toLowerCase(),
5326
5772
  tokenInAmount: swapRequest.tokenInAmount.toString(),
5327
5773
  slippageBps,
5328
- routing: "CLASSIC",
5774
+ routing,
5775
+ executionKind: isUniswapX ? "uniswapx" : "classic",
5776
+ routerVersion: routerProfile.routerVersion,
5329
5777
  })
5330
5778
  );
5331
5779
  return {
@@ -5340,13 +5788,25 @@ export class WdkEvmWalletService {
5340
5788
  tokenOutAmount,
5341
5789
  minimumTokenOutAmount,
5342
5790
  slippageBps,
5791
+ routing,
5792
+ executionKind: isUniswapX ? "uniswapx" : "classic",
5793
+ isUniswapX,
5343
5794
  quoteFingerprint,
5344
5795
  gasFee: quoteResponse.quote.gasFee ?? null,
5345
5796
  gasFeeUSD: quoteResponse.quote.gasFeeUSD ?? null,
5346
5797
  router,
5798
+ routerProfile,
5799
+ preparedTransaction: null,
5347
5800
  };
5348
5801
  }
5349
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
+
5350
5810
  async #signUniswapPermit(account, permitData, runtimeConfig) {
5351
5811
  const typed = normalizeUniswapPermitData(permitData, runtimeConfig);
5352
5812
  return account.signTypedData({
@@ -5356,7 +5816,13 @@ export class WdkEvmWalletService {
5356
5816
  });
5357
5817
  }
5358
5818
 
5359
- async #fetchUniswapSwapCalldata({ runtimeConfig, quoteResponse, permitData, signature }) {
5819
+ async #fetchUniswapSwapCalldata({
5820
+ runtimeConfig,
5821
+ routerProfile,
5822
+ quoteResponse,
5823
+ permitData,
5824
+ signature,
5825
+ }) {
5360
5826
  const { permitData: _permitData, permitTransaction: _permitTransaction, ...cleanQuote } =
5361
5827
  quoteResponse;
5362
5828
  const body = { ...cleanQuote };
@@ -5366,10 +5832,13 @@ export class WdkEvmWalletService {
5366
5832
  body.signature = signature;
5367
5833
  body.permitData = permitData;
5368
5834
  }
5369
- const payload = await this.#uniswapTradingApiRequest("/swap", body);
5835
+ const payload = await this.#uniswapTradingApiRequest("/swap", body, {
5836
+ routerVersion: routerProfile.routerVersion,
5837
+ chainId: runtimeConfig.chainId,
5838
+ });
5370
5839
  const swap = payload.swap || {};
5371
5840
  const to = normalizeAddress(String(swap.to || ""), "swap.to");
5372
- const expectedRouter = UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK[runtimeConfig.network];
5841
+ const expectedRouter = routerProfile.router;
5373
5842
  if (to.toLowerCase() !== expectedRouter) {
5374
5843
  throw createTaggedError(
5375
5844
  "Uniswap /swap returned an unexpected target contract.",
@@ -5378,6 +5847,7 @@ export class WdkEvmWalletService {
5378
5847
  );
5379
5848
  }
5380
5849
  const data = assertNonEmptyString(String(swap.data || ""), "swap.data");
5850
+ const value = parseHexOrDecimalBigInt(swap.value || "0", "swap.value");
5381
5851
  if (data === "0x") {
5382
5852
  throw createTaggedError(
5383
5853
  "Uniswap /swap returned empty calldata. The quote likely expired; generate a new preview.",
@@ -5388,7 +5858,38 @@ export class WdkEvmWalletService {
5388
5858
  return {
5389
5859
  to,
5390
5860
  data,
5391
- 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"),
5392
5893
  };
5393
5894
  }
5394
5895
 
@@ -7558,8 +8059,14 @@ export class WdkEvmWalletService {
7558
8059
  export const __testables = {
7559
8060
  PERMIT2_ADDRESS,
7560
8061
  UNISWAP_SUPPORTED_CHAIN_IDS,
8062
+ UNISWAP_EXECUTION_PROFILES,
8063
+ UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK,
8064
+ UNISWAP_WRAPPED_NATIVE_BY_NETWORK,
7561
8065
  normalizeUniswapTokenAddress,
7562
8066
  assertUniswapSupportedNetwork,
8067
+ resolveUniswapRouterExecutionProfile,
8068
+ buildDirectWrappedNativeOperation,
8069
+ assertValidNetwork,
7563
8070
  uniswapSlippagePercentFromBps,
7564
8071
  normalizeUniswapPermitData,
7565
8072
  };