@agentlayer.tech/wallet 0.1.77 → 0.1.88

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.
@@ -23,12 +23,68 @@ 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
+ // Robinhood Chain has no Universal Router 2.0 deployment - Uniswap
45
+ // defaults it (like Ink) to 2.1.1, and requesting "2.0" makes the Trading
46
+ // API return a blanket "No quotes available" for every pair on this
47
+ // chain. See https://blog.uniswap.org/robinhood-chain-is-live. Same
48
+ // contract address as before; only the version label changes.
49
+ universalRouters: { "2.1.1": "0x8876789976decbfcbbbe364623c63652db8c0904" },
50
+ wrappedNative: "0x0bd7d308f8e1639fab988df18a8011f41eacad73",
51
+ // Full protocol stack like ethereum/base - the earlier V3-only filter
52
+ // wasn't the actual blocker (see router version note above) but there's
53
+ // no reason to hide V2/V4 routes from the Trading API either.
54
+ ammProtocols: ["V2", "V3", "V4"],
55
+ v3DirectFallback: {
56
+ quoterV2: "0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7",
57
+ swapRouter02: "0xcaf681a66d020601342297493863e78c959e5cb2",
58
+ feeTiers: [100, 500, 3000, 10000],
59
+ },
60
+ },
31
61
  };
62
+ // Compatibility export for callers/tests. New execution code resolves a router
63
+ // from UNISWAP_EXECUTION_PROFILES and its explicitly selected version.
64
+ const UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK = Object.fromEntries(
65
+ Object.entries(UNISWAP_EXECUTION_PROFILES).map(([network, profile]) => [
66
+ network,
67
+ profile.universalRouters["2.0"],
68
+ ])
69
+ );
70
+ const UNISWAP_WRAPPED_NATIVE_BY_NETWORK = Object.fromEntries(
71
+ Object.entries(UNISWAP_EXECUTION_PROFILES).map(([network, profile]) => [
72
+ network,
73
+ profile.wrappedNative,
74
+ ])
75
+ );
76
+ const WETH_DEPOSIT_SELECTOR = "0xd0e30db0";
77
+ const WETH_WITHDRAW_SELECTOR = "0x2e1a7d4d";
78
+ const UNISWAP_V3_QUOTER_INTERFACE = new Interface([
79
+ "function quoteExactInputSingle((address tokenIn,address tokenOut,uint256 amountIn,uint24 fee,uint160 sqrtPriceLimitX96) params) external returns (uint256 amountOut,uint160 sqrtPriceX96After,uint32 initializedTicksCrossed,uint256 gasEstimate)",
80
+ ]);
81
+ const UNISWAP_V3_SWAP_ROUTER_INTERFACE = new Interface([
82
+ "function exactInputSingle((address tokenIn,address tokenOut,uint24 fee,address recipient,uint256 amountIn,uint256 amountOutMinimum,uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)",
83
+ "function unwrapWETH9(uint256 amountMinimum,address recipient) payable",
84
+ "function multicall(bytes[] data) payable returns (bytes[] results)",
85
+ ]);
86
+ const UNISWAP_CLASSIC_ROUTINGS = new Set(["CLASSIC"]);
87
+ const UNISWAPX_ROUTINGS = new Set(["DUTCH_V2", "DUTCH_V3", "LIMIT_ORDER", "PRIORITY"]);
32
88
  const AAVE_RAY = 10n ** 27n;
33
89
  const LIDO_STETH_DECIMALS = 18;
34
90
  const LIDO_MIN_STETH_WITHDRAWAL_AMOUNT = 100n;
@@ -104,8 +160,20 @@ const MORPHO_MAX_LIST_LIMIT = 500;
104
160
  // two minutes is below the indexer's own aggregation noise.
105
161
  const MORPHO_DISCOVERY_CACHE_TTL_MS = 120_000;
106
162
  const MORPHO_DISCOVERY_CACHE_MAX_ENTRIES = 64;
163
+ // DeFi routes can take a different on-chain branch between estimate time and
164
+ // inclusion. Estimate exact final calldata immediately before signing and use
165
+ // an explicit headroom rather than relying on the provider/WDK default.
166
+ const DEFI_GAS_BUFFER_BPS = 13_000n;
167
+ const MORPHO_GAS_BUFFER_BPS = DEFI_GAS_BUFFER_BPS;
168
+ const BPS_DENOMINATOR = 10_000n;
107
169
  const morphoDiscoveryCache = new Map();
108
170
 
171
+ function applyGasBuffer(value, bufferBps = MORPHO_GAS_BUFFER_BPS) {
172
+ const normalizedValue = BigInt(value);
173
+ const normalizedBufferBps = BigInt(bufferBps);
174
+ return (normalizedValue * normalizedBufferBps + BPS_DENOMINATOR - 1n) / BPS_DENOMINATOR;
175
+ }
176
+
109
177
  function morphoDiscoveryCacheGet(key) {
110
178
  const entry = morphoDiscoveryCache.get(key);
111
179
  if (!entry) {
@@ -857,6 +925,115 @@ function assertUniswapSupportedNetwork(network) {
857
925
  return chainId;
858
926
  }
859
927
 
928
+ function getUniswapNetworkExecutionProfile(network) {
929
+ const profile = UNISWAP_EXECUTION_PROFILES[network];
930
+ if (!profile) {
931
+ assertUniswapSupportedNetwork(network);
932
+ }
933
+ return profile;
934
+ }
935
+
936
+ function resolveUniswapRouterExecutionProfile(network, routerVersion) {
937
+ const networkProfile = getUniswapNetworkExecutionProfile(network);
938
+ const version = String(routerVersion || "2.0").trim() || "2.0";
939
+ const router = networkProfile.universalRouters[version];
940
+ if (!router) {
941
+ throw createTaggedError(
942
+ `Uniswap Universal Router version ${version} is not approved for ${network}.`,
943
+ "uniswap_router_version_unsupported",
944
+ {
945
+ network,
946
+ chainId: networkProfile.chainId,
947
+ requestedVersion: version,
948
+ supportedVersions: Object.keys(networkProfile.universalRouters),
949
+ }
950
+ );
951
+ }
952
+ return { ...networkProfile, routerVersion: version, router };
953
+ }
954
+
955
+ function encodeUint256(value) {
956
+ return BigInt(value).toString(16).padStart(64, "0");
957
+ }
958
+
959
+ function buildDirectWrappedNativeOperation(networkProfile, swapRequest) {
960
+ const wrappedNative = networkProfile.wrappedNative;
961
+ if (isZeroAddress(swapRequest.tokenIn) && swapRequest.tokenOut === wrappedNative) {
962
+ return {
963
+ executionKind: "wrap",
964
+ routing: "WRAP",
965
+ transaction: { to: wrappedNative, data: WETH_DEPOSIT_SELECTOR, value: swapRequest.tokenInAmount },
966
+ };
967
+ }
968
+ if (swapRequest.tokenIn === wrappedNative && isZeroAddress(swapRequest.tokenOut)) {
969
+ return {
970
+ executionKind: "unwrap",
971
+ routing: "UNWRAP",
972
+ transaction: {
973
+ to: wrappedNative,
974
+ data: `${WETH_WITHDRAW_SELECTOR}${encodeUint256(swapRequest.tokenInAmount)}`,
975
+ value: 0n,
976
+ },
977
+ };
978
+ }
979
+ return null;
980
+ }
981
+
982
+ function buildUniswapV3DirectSwapTransaction({
983
+ networkProfile,
984
+ recipient,
985
+ swapRequest,
986
+ feeTier,
987
+ minimumTokenOutAmount,
988
+ }) {
989
+ const fallback = networkProfile.v3DirectFallback;
990
+ if (!fallback) {
991
+ throw new Error("Uniswap V3 direct fallback is not configured for this network.");
992
+ }
993
+ const nativeTokenIn = isZeroAddress(swapRequest.tokenIn);
994
+ const nativeTokenOut = isZeroAddress(swapRequest.tokenOut);
995
+ const effectiveTokenIn = nativeTokenIn ? networkProfile.wrappedNative : swapRequest.tokenIn;
996
+ const effectiveTokenOut = nativeTokenOut ? networkProfile.wrappedNative : swapRequest.tokenOut;
997
+ const exactInput = UNISWAP_V3_SWAP_ROUTER_INTERFACE.encodeFunctionData("exactInputSingle", [
998
+ {
999
+ tokenIn: effectiveTokenIn,
1000
+ tokenOut: effectiveTokenOut,
1001
+ fee: feeTier,
1002
+ // For an ETH output the router must receive WETH first, then unwrap it in
1003
+ // the same atomic multicall. All other paths transfer directly to the user.
1004
+ recipient: nativeTokenOut ? fallback.swapRouter02 : recipient,
1005
+ amountIn: swapRequest.tokenInAmount,
1006
+ amountOutMinimum: minimumTokenOutAmount,
1007
+ sqrtPriceLimitX96: 0,
1008
+ },
1009
+ ]);
1010
+ const data = nativeTokenOut
1011
+ ? UNISWAP_V3_SWAP_ROUTER_INTERFACE.encodeFunctionData("multicall(bytes[])", [
1012
+ [
1013
+ exactInput,
1014
+ UNISWAP_V3_SWAP_ROUTER_INTERFACE.encodeFunctionData("unwrapWETH9", [
1015
+ minimumTokenOutAmount,
1016
+ recipient,
1017
+ ]),
1018
+ ],
1019
+ ])
1020
+ : exactInput;
1021
+ return {
1022
+ to: fallback.swapRouter02,
1023
+ data,
1024
+ value: nativeTokenIn ? swapRequest.tokenInAmount : 0n,
1025
+ };
1026
+ }
1027
+
1028
+ function isUniswapNoQuotesAvailableError(error) {
1029
+ return (
1030
+ error &&
1031
+ typeof error === "object" &&
1032
+ error.errorCode === "network_unavailable" &&
1033
+ /no quotes available/i.test(String(error.message || ""))
1034
+ );
1035
+ }
1036
+
860
1037
  function uniswapSlippagePercentFromBps(bps) {
861
1038
  const parsed = Number(bps);
862
1039
  if (!Number.isInteger(parsed) || parsed < 0 || parsed > 5000) {
@@ -2352,12 +2529,20 @@ export class WdkEvmWalletService {
2352
2529
  const protocol = this.#createMorphoProtocol(account, runtimeConfig, request);
2353
2530
  let result;
2354
2531
  try {
2355
- result = await protocol[this.#getMorphoOperationMethods(request).sendMethod](
2356
- this.#buildMorphoOperationOptions(request)
2357
- );
2532
+ result = await this.#sendMorphoProtocolOperation({
2533
+ account,
2534
+ runtimeConfig,
2535
+ protocol,
2536
+ request,
2537
+ });
2358
2538
  } finally {
2359
2539
  await maybeDispose(protocol);
2360
2540
  }
2541
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
2542
+ operationLabel: "Morpho operation",
2543
+ failureCode: "morpho_operation_reverted",
2544
+ timeoutCode: "morpho_operation_confirmation_timeout",
2545
+ });
2361
2546
  const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
2362
2547
  const totalFee = requirementExecution.totalFee + resultFee;
2363
2548
  return {
@@ -2383,6 +2568,7 @@ export class WdkEvmWalletService {
2383
2568
  requirementsFee: requirementExecution.totalFee.toString(),
2384
2569
  requirements: requirementExecution.transactions,
2385
2570
  },
2571
+ confirmed: true,
2386
2572
  };
2387
2573
  } catch (error) {
2388
2574
  const cleanup = await this.#restoreMorphoRequirementsAfterFailedOperation({
@@ -2529,12 +2715,20 @@ export class WdkEvmWalletService {
2529
2715
  const protocol = this.#createMorphoProtocol(account, runtimeConfig, request);
2530
2716
  let result;
2531
2717
  try {
2532
- result = await protocol[this.#getMorphoOperationMethods(request).sendMethod](
2533
- this.#buildMorphoOperationOptions(request)
2534
- );
2718
+ result = await this.#sendMorphoProtocolOperation({
2719
+ account,
2720
+ runtimeConfig,
2721
+ protocol,
2722
+ request,
2723
+ });
2535
2724
  } finally {
2536
2725
  await maybeDispose(protocol);
2537
2726
  }
2727
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
2728
+ operationLabel: "Morpho operation",
2729
+ failureCode: "morpho_operation_reverted",
2730
+ timeoutCode: "morpho_operation_confirmation_timeout",
2731
+ });
2538
2732
  const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
2539
2733
  const totalFee = requirementExecution.totalFee + resultFee;
2540
2734
  return {
@@ -2560,6 +2754,7 @@ export class WdkEvmWalletService {
2560
2754
  requirementsFee: requirementExecution.totalFee.toString(),
2561
2755
  requirements: requirementExecution.transactions,
2562
2756
  },
2757
+ confirmed: true,
2563
2758
  };
2564
2759
  } catch (error) {
2565
2760
  const cleanup = await this.#restoreMorphoRequirementsAfterFailedOperation({
@@ -2696,13 +2891,25 @@ export class WdkEvmWalletService {
2696
2891
  const protocol = new AaveProtocolEvm(account);
2697
2892
  let result;
2698
2893
  try {
2699
- result = await protocol[request.operation]({
2700
- token: request.token,
2701
- amount: request.amount,
2894
+ result = await this.#sendBufferedDefiProtocolOperation({
2895
+ account,
2896
+ runtimeConfig,
2897
+ operationLabel: `Aave ${request.operation}`,
2898
+ operation: request.operation,
2899
+ invoke: () =>
2900
+ protocol[request.operation]({
2901
+ token: request.token,
2902
+ amount: request.amount,
2903
+ }),
2702
2904
  });
2703
2905
  } finally {
2704
2906
  await maybeDispose(protocol);
2705
2907
  }
2908
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
2909
+ operationLabel: "Aave operation",
2910
+ failureCode: "aave_operation_reverted",
2911
+ timeoutCode: "aave_operation_confirmation_timeout",
2912
+ });
2706
2913
  const resultFee = BigInt(result?.fee || 0);
2707
2914
  const totalFee = approvalExecution.totalFee + resultFee;
2708
2915
  return {
@@ -2731,6 +2938,7 @@ export class WdkEvmWalletService {
2731
2938
  ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
2732
2939
  : {}),
2733
2940
  },
2941
+ confirmed: true,
2734
2942
  };
2735
2943
  } catch (error) {
2736
2944
  const cleanup = await this.#restoreAllowanceAfterFailedAaveOperation({
@@ -3012,7 +3220,19 @@ export class WdkEvmWalletService {
3012
3220
  );
3013
3221
  }
3014
3222
 
3015
- const result = await account.sendTransaction(finalPlan.operationTx);
3223
+ const result = await this.#sendBufferedDefiTransaction({
3224
+ account,
3225
+ runtimeConfig,
3226
+ from: address,
3227
+ tx: finalPlan.operationTx,
3228
+ operationLabel: `Lido ${request.operation}`,
3229
+ operation: request.operation,
3230
+ });
3231
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
3232
+ operationLabel: "Lido operation",
3233
+ failureCode: "lido_operation_reverted",
3234
+ timeoutCode: "lido_operation_confirmation_timeout",
3235
+ });
3016
3236
  const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
3017
3237
  const totalFee = approvalExecution.totalFee + resultFee;
3018
3238
  return {
@@ -3041,6 +3261,7 @@ export class WdkEvmWalletService {
3041
3261
  ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
3042
3262
  : {}),
3043
3263
  },
3264
+ confirmed: true,
3044
3265
  };
3045
3266
  } catch (error) {
3046
3267
  const cleanup = await this.#restoreAllowanceAfterFailedLidoOperation({
@@ -3163,7 +3384,19 @@ export class WdkEvmWalletService {
3163
3384
  );
3164
3385
  }
3165
3386
 
3166
- const result = await account.sendTransaction(finalPlan.operationTx);
3387
+ const result = await this.#sendBufferedDefiTransaction({
3388
+ account,
3389
+ runtimeConfig,
3390
+ from: address,
3391
+ tx: finalPlan.operationTx,
3392
+ operationLabel: `Lido ${request.operation}`,
3393
+ operation: request.operation,
3394
+ });
3395
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
3396
+ operationLabel: "Lido withdrawal",
3397
+ failureCode: "lido_withdrawal_reverted",
3398
+ timeoutCode: "lido_withdrawal_confirmation_timeout",
3399
+ });
3167
3400
  const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
3168
3401
  const totalFee = approvalExecution.totalFee + resultFee;
3169
3402
  return {
@@ -3192,6 +3425,7 @@ export class WdkEvmWalletService {
3192
3425
  ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
3193
3426
  : {}),
3194
3427
  },
3428
+ confirmed: true,
3195
3429
  };
3196
3430
  } catch (error) {
3197
3431
  const cleanup = await this.#restoreAllowanceAfterFailedLidoWithdrawal({
@@ -3424,12 +3658,23 @@ export class WdkEvmWalletService {
3424
3658
  })
3425
3659
  : finalPlan.simulation;
3426
3660
  this.#assertSimulationSucceeded(effectiveSimulation);
3427
- const { hash } = await account.sendTransaction(finalPlan.swapTx);
3428
- const totalFee = approvalExecution.totalFee + finalPlan.swapFee;
3661
+ const swapResult = await this.#sendBufferedDefiTransaction({
3662
+ account,
3663
+ runtimeConfig,
3664
+ from: address,
3665
+ tx: finalPlan.swapTx,
3666
+ operationLabel: "Velora swap",
3667
+ });
3668
+ await this.#waitForTransactionReceipt(runtimeConfig, swapResult.hash, {
3669
+ operationLabel: "Velora swap",
3670
+ failureCode: "swap_reverted",
3671
+ timeoutCode: "swap_confirmation_timeout",
3672
+ });
3673
+ const totalFee = approvalExecution.totalFee + BigInt(swapResult.fee);
3429
3674
  const result = {
3430
- hash,
3675
+ ...swapResult,
3431
3676
  fee: totalFee.toString(),
3432
- swapFee: finalPlan.swapFee.toString(),
3677
+ swapFee: swapResult.fee,
3433
3678
  approvalFee: approvalExecution.totalFee.toString(),
3434
3679
  tokenInAmount: finalPlan.tokenInAmount.toString(),
3435
3680
  tokenOutAmount: finalPlan.tokenOutAmount.toString(),
@@ -3470,6 +3715,7 @@ export class WdkEvmWalletService {
3470
3715
  simulation: effectiveSimulation,
3471
3716
  swapTransaction: finalPlan.swapTransaction,
3472
3717
  result,
3718
+ confirmed: true,
3473
3719
  source: "wdk-protocol-swap-velora-evm",
3474
3720
  };
3475
3721
  } catch (error) {
@@ -3580,12 +3826,23 @@ export class WdkEvmWalletService {
3580
3826
  : finalPlan.simulation;
3581
3827
  this.#assertSimulationSucceeded(effectiveSimulation);
3582
3828
 
3583
- const { hash } = await account.sendTransaction(finalPlan.swapTx);
3584
- const totalFee = approvalExecution.totalFee + finalPlan.swapFee;
3829
+ const swapResult = await this.#sendBufferedDefiTransaction({
3830
+ account,
3831
+ runtimeConfig,
3832
+ from: sourceAddress,
3833
+ tx: finalPlan.swapTx,
3834
+ operationLabel: "LI.FI swap",
3835
+ });
3836
+ await this.#waitForTransactionReceipt(runtimeConfig, swapResult.hash, {
3837
+ operationLabel: "LI.FI swap",
3838
+ failureCode: "swap_reverted",
3839
+ timeoutCode: "swap_confirmation_timeout",
3840
+ });
3841
+ const totalFee = approvalExecution.totalFee + BigInt(swapResult.fee);
3585
3842
  const result = {
3586
- hash,
3843
+ ...swapResult,
3587
3844
  fee: totalFee.toString(),
3588
- swapFee: finalPlan.swapFee.toString(),
3845
+ swapFee: swapResult.fee,
3589
3846
  approvalFee: approvalExecution.totalFee.toString(),
3590
3847
  tokenInAmount: finalPlan.tokenInAmount.toString(),
3591
3848
  tokenOutAmount: finalPlan.tokenOutAmount.toString(),
@@ -3612,6 +3869,7 @@ export class WdkEvmWalletService {
3612
3869
  },
3613
3870
  }),
3614
3871
  result,
3872
+ confirmed: true,
3615
3873
  };
3616
3874
  } catch (error) {
3617
3875
  const cleanup = await this.#restoreAllowanceAfterFailedSwap({
@@ -3757,36 +4015,84 @@ export class WdkEvmWalletService {
3757
4015
  );
3758
4016
  }
3759
4017
 
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
- });
4018
+ const signature = finalPlan.permitData
4019
+ ? await this.#signUniswapPermit(account, finalPlan.permitData, runtimeConfig)
4020
+ : null;
3771
4021
 
3772
- const simulation = await this.#simulatePreparedTransaction({
3773
- runtimeConfig,
3774
- from: address,
3775
- tx: swapTx,
3776
- });
3777
- this.#assertSimulationSucceeded(simulation);
4022
+ let simulation;
4023
+ let swapTransaction = null;
4024
+ let result;
4025
+ if (finalPlan.isUniswapX) {
4026
+ const order = await this.#submitUniswapOrder({
4027
+ quoteResponse: finalPlan.quoteResponse,
4028
+ signature,
4029
+ nativeTokenIn: finalPlan.isNativeTokenIn,
4030
+ routing: finalPlan.routing,
4031
+ routerProfile: finalPlan.routerProfile,
4032
+ });
4033
+ simulation = {
4034
+ ok: true,
4035
+ skipped: true,
4036
+ reason: "uniswapx-order",
4037
+ message: "UniswapX validates and submits the signed order to its filler network.",
4038
+ details: null,
4039
+ };
4040
+ result = {
4041
+ orderId: order.orderId,
4042
+ orderStatus: order.orderStatus,
4043
+ approvalFee: approvalExecution.totalFee.toString(),
4044
+ tokenInAmount: finalPlan.tokenInAmount.toString(),
4045
+ tokenOutAmount: finalPlan.tokenOutAmount.toString(),
4046
+ ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
4047
+ ...(approvalExecution.resetAllowanceHash
4048
+ ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
4049
+ : {}),
4050
+ };
4051
+ } else {
4052
+ const swapTx =
4053
+ finalPlan.preparedTransaction ||
4054
+ (await this.#fetchUniswapSwapCalldata({
4055
+ runtimeConfig,
4056
+ routerProfile: finalPlan.routerProfile,
4057
+ quoteResponse: finalPlan.quoteResponse,
4058
+ permitData: finalPlan.permitData,
4059
+ signature,
4060
+ }));
4061
+ simulation = await this.#simulatePreparedTransaction({
4062
+ runtimeConfig,
4063
+ from: address,
4064
+ tx: swapTx,
4065
+ });
4066
+ this.#assertSimulationSucceeded(simulation);
3778
4067
 
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
- };
4068
+ const swapResult = await this.#sendBufferedDefiTransaction({
4069
+ account,
4070
+ runtimeConfig,
4071
+ from: address,
4072
+ tx: swapTx,
4073
+ operationLabel: "Uniswap swap",
4074
+ });
4075
+ await this.#waitForTransactionReceipt(runtimeConfig, swapResult.hash, {
4076
+ operationLabel: "Uniswap swap",
4077
+ failureCode: "swap_reverted",
4078
+ timeoutCode: "swap_confirmation_timeout",
4079
+ });
4080
+ swapTransaction = {
4081
+ to: swapTx.to,
4082
+ value: swapTx.value.toString(),
4083
+ dataHash: sha256Hex(swapTx.data),
4084
+ };
4085
+ result = {
4086
+ ...swapResult,
4087
+ approvalFee: approvalExecution.totalFee.toString(),
4088
+ tokenInAmount: finalPlan.tokenInAmount.toString(),
4089
+ tokenOutAmount: finalPlan.tokenOutAmount.toString(),
4090
+ ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
4091
+ ...(approvalExecution.resetAllowanceHash
4092
+ ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
4093
+ : {}),
4094
+ };
4095
+ }
3790
4096
  return {
3791
4097
  ...(await this.#formatUniswapSwapResponse({
3792
4098
  runtimeConfig,
@@ -3802,8 +4108,9 @@ export class WdkEvmWalletService {
3802
4108
  },
3803
4109
  })),
3804
4110
  simulation,
3805
- swapTransaction: { to: swapTx.to, value: swapTx.value.toString(), dataHash: sha256Hex(swapTx.data) },
4111
+ swapTransaction,
3806
4112
  result,
4113
+ confirmed: Boolean(result.hash),
3807
4114
  };
3808
4115
  } catch (error) {
3809
4116
  const cleanup = await this.#restoreAllowanceAfterFailedSwap({
@@ -3845,7 +4152,8 @@ export class WdkEvmWalletService {
3845
4152
  address,
3846
4153
  protocol: "uniswap",
3847
4154
  executionSupported: true,
3848
- routing: "CLASSIC",
4155
+ executionKind: plan.executionKind,
4156
+ routing: plan.routing,
3849
4157
  swapRequest: {
3850
4158
  tokenIn: swapRequest.tokenIn,
3851
4159
  tokenOut: swapRequest.tokenOut,
@@ -3854,6 +4162,7 @@ export class WdkEvmWalletService {
3854
4162
  tokenInMetadata,
3855
4163
  tokenOutMetadata,
3856
4164
  inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
4165
+ outputAmount: plan.tokenOutAmount.toString(),
3857
4166
  outputAmountFormatted: formatUnits(plan.tokenOutAmount, tokenOutMetadata.decimals),
3858
4167
  quoteFingerprint: plan.quoteFingerprint,
3859
4168
  slippageBps: plan.slippageBps,
@@ -3871,7 +4180,19 @@ export class WdkEvmWalletService {
3871
4180
  readError: plan.allowanceReadError,
3872
4181
  },
3873
4182
  router: plan.router,
3874
- source: "uniswap-trading-api",
4183
+ swapTransaction: plan.preparedTransaction
4184
+ ? {
4185
+ to: plan.preparedTransaction.to,
4186
+ value: plan.preparedTransaction.value.toString(),
4187
+ dataHash: sha256Hex(plan.preparedTransaction.data),
4188
+ }
4189
+ : null,
4190
+ source:
4191
+ plan.executionKind === "wrap" || plan.executionKind === "unwrap"
4192
+ ? "canonical-wrapped-native"
4193
+ : plan.executionKind === "v3-direct"
4194
+ ? "uniswap-v3-direct-fallback"
4195
+ : "uniswap-trading-api",
3875
4196
  };
3876
4197
  }
3877
4198
 
@@ -4325,6 +4646,150 @@ export class WdkEvmWalletService {
4325
4646
  return options;
4326
4647
  }
4327
4648
 
4649
+ async #sendMorphoProtocolOperation({ account, runtimeConfig, protocol, request }) {
4650
+ const methods = this.#getMorphoOperationMethods(request);
4651
+ const originalSendTransaction = account.sendTransaction;
4652
+ if (typeof originalSendTransaction !== "function") {
4653
+ throw new Error("Morpho operation requires an EVM account with sendTransaction().");
4654
+ }
4655
+ const hadOwnSendTransaction = Object.prototype.hasOwnProperty.call(account, "sendTransaction");
4656
+
4657
+ // The protocol constructs the final calldata immediately before sending it.
4658
+ // Intercept that one protocol-scoped send so the estimate and padded limit
4659
+ // are based on the exact transaction, rather than a stale preview.
4660
+ account.sendTransaction = async (tx) => {
4661
+ const gas = await this.#prepareMorphoGasLimit({
4662
+ runtimeConfig,
4663
+ from: await account.getAddress(),
4664
+ tx,
4665
+ operation: request.operation,
4666
+ });
4667
+ const result = await originalSendTransaction.call(account, {
4668
+ ...tx,
4669
+ gasLimit: gas.gasLimit,
4670
+ });
4671
+ return {
4672
+ ...result,
4673
+ fee: gas.maximumFee,
4674
+ gasEstimate: gas.gasEstimate.toString(),
4675
+ gasLimit: gas.gasLimit.toString(),
4676
+ gasBufferBps: MORPHO_GAS_BUFFER_BPS.toString(),
4677
+ };
4678
+ };
4679
+
4680
+ try {
4681
+ return await protocol[methods.sendMethod](this.#buildMorphoOperationOptions(request));
4682
+ } finally {
4683
+ if (hadOwnSendTransaction) {
4684
+ account.sendTransaction = originalSendTransaction;
4685
+ } else {
4686
+ delete account.sendTransaction;
4687
+ }
4688
+ }
4689
+ }
4690
+
4691
+ async #prepareMorphoGasLimit({ runtimeConfig, from, tx, operation }) {
4692
+ return this.#prepareBufferedDefiGasLimit({
4693
+ runtimeConfig,
4694
+ from,
4695
+ tx,
4696
+ operationLabel: `morpho ${operation}`,
4697
+ unavailableCode: "morpho_gas_estimate_unavailable",
4698
+ unavailableMessage: "Morpho gas estimate was empty. Generate a new quote before sending.",
4699
+ });
4700
+ }
4701
+
4702
+ async #prepareBufferedDefiGasLimit({
4703
+ runtimeConfig,
4704
+ from,
4705
+ tx,
4706
+ operationLabel,
4707
+ unavailableCode = "defi_gas_estimate_unavailable",
4708
+ unavailableMessage = "DeFi gas estimate was empty. Generate a new quote before sending.",
4709
+ }) {
4710
+ const gasEstimateHex = await rpcRequest(runtimeConfig.providerUrl, "eth_estimateGas", [
4711
+ {
4712
+ from: normalizeAddress(from, "from"),
4713
+ to: normalizeAddress(String(tx.to || ""), "to"),
4714
+ data: assertNonEmptyString(String(tx.data || ""), "data"),
4715
+ value: toRpcHex(tx.value || 0),
4716
+ },
4717
+ ]);
4718
+ const gasEstimate = BigInt(gasEstimateHex || "0x0");
4719
+ if (gasEstimate <= 0n) {
4720
+ throw createTaggedError(unavailableMessage, unavailableCode, { operation: operationLabel });
4721
+ }
4722
+ const gasLimit = applyGasBuffer(gasEstimate, DEFI_GAS_BUFFER_BPS);
4723
+ const effectiveFeePerGas = await this.#getEffectiveGasPrice(runtimeConfig);
4724
+ const maximumFee = gasLimit * effectiveFeePerGas;
4725
+ this.#assertMaxFee(runtimeConfig, maximumFee, operationLabel);
4726
+ return { gasEstimate, gasLimit, maximumFee };
4727
+ }
4728
+
4729
+ async #sendBufferedDefiTransaction({ account, runtimeConfig, from, tx, operationLabel }) {
4730
+ return this.#sendBufferedDefiTransactionWithSender({
4731
+ sendTransaction: (preparedTx) => account.sendTransaction(preparedTx),
4732
+ runtimeConfig,
4733
+ from,
4734
+ tx,
4735
+ operationLabel,
4736
+ });
4737
+ }
4738
+
4739
+ async #sendBufferedDefiTransactionWithSender({
4740
+ sendTransaction,
4741
+ runtimeConfig,
4742
+ from,
4743
+ tx,
4744
+ operationLabel,
4745
+ }) {
4746
+ const gas = await this.#prepareBufferedDefiGasLimit({
4747
+ runtimeConfig,
4748
+ from,
4749
+ tx,
4750
+ operationLabel,
4751
+ });
4752
+ const result = await sendTransaction({ ...tx, gasLimit: gas.gasLimit });
4753
+ return {
4754
+ ...result,
4755
+ fee: gas.maximumFee.toString(),
4756
+ gasEstimate: gas.gasEstimate.toString(),
4757
+ gasLimit: gas.gasLimit.toString(),
4758
+ gasBufferBps: DEFI_GAS_BUFFER_BPS.toString(),
4759
+ };
4760
+ }
4761
+
4762
+ async #sendBufferedDefiProtocolOperation({
4763
+ account,
4764
+ runtimeConfig,
4765
+ operationLabel,
4766
+ operation,
4767
+ invoke,
4768
+ }) {
4769
+ const originalSendTransaction = account.sendTransaction;
4770
+ if (typeof originalSendTransaction !== "function") {
4771
+ throw new Error(`${operationLabel} requires an EVM account with sendTransaction().`);
4772
+ }
4773
+ const hadOwnSendTransaction = Object.prototype.hasOwnProperty.call(account, "sendTransaction");
4774
+ account.sendTransaction = async (tx) =>
4775
+ this.#sendBufferedDefiTransactionWithSender({
4776
+ sendTransaction: (preparedTx) => originalSendTransaction.call(account, preparedTx),
4777
+ runtimeConfig,
4778
+ from: await account.getAddress(),
4779
+ tx,
4780
+ operationLabel: `${operationLabel} ${operation}`,
4781
+ });
4782
+ try {
4783
+ return await invoke();
4784
+ } finally {
4785
+ if (hadOwnSendTransaction) {
4786
+ account.sendTransaction = originalSendTransaction;
4787
+ } else {
4788
+ delete account.sendTransaction;
4789
+ }
4790
+ }
4791
+ }
4792
+
4328
4793
  async #buildMorphoOperationPlan({
4329
4794
  account,
4330
4795
  runtimeConfig,
@@ -4525,7 +4990,7 @@ export class WdkEvmWalletService {
4525
4990
  try {
4526
4991
  const quote = await protocol[methods.quoteMethod](operationOptions);
4527
4992
  return {
4528
- fee: BigInt(quote?.fee || 0),
4993
+ fee: applyGasBuffer(BigInt(quote?.fee || 0), MORPHO_GAS_BUFFER_BPS),
4529
4994
  error: null,
4530
4995
  };
4531
4996
  } catch (error) {
@@ -4622,11 +5087,23 @@ export class WdkEvmWalletService {
4622
5087
  for (let index = 0; index < plan.requirements.transactions.length; index += 1) {
4623
5088
  const requirementTx = plan.requirements.transactions[index];
4624
5089
  const step = plan.requirements.steps[index];
4625
- const result = await account.sendTransaction({
5090
+ const tx = {
4626
5091
  to: requirementTx.to,
4627
5092
  value: requirementTx.value ?? 0n,
4628
5093
  data: requirementTx.data,
4629
- });
5094
+ };
5095
+ // ERC-20 approval writes intentionally retain the narrow WDK path. A
5096
+ // Morpho authorization is a protocol write, so give it the same buffer
5097
+ // as the potentially stateful final operation.
5098
+ const result = step?.type === "authorization"
5099
+ ? await this.#sendBufferedDefiTransaction({
5100
+ account,
5101
+ runtimeConfig,
5102
+ from: await account.getAddress(),
5103
+ tx,
5104
+ operationLabel: "Morpho authorization",
5105
+ })
5106
+ : await account.sendTransaction(tx);
4630
5107
  const fee = BigInt(result?.fee || 0);
4631
5108
  totalFee += fee;
4632
5109
  transactions.push({
@@ -4702,13 +5179,19 @@ export class WdkEvmWalletService {
4702
5179
  }
4703
5180
 
4704
5181
  for (const context of requirementExecution.authorizationContexts || []) {
4705
- const result = await account.sendTransaction({
5182
+ const result = await this.#sendBufferedDefiTransaction({
5183
+ account,
5184
+ runtimeConfig,
5185
+ from: await account.getAddress(),
5186
+ operationLabel: "Morpho authorization cleanup",
5187
+ tx: {
4706
5188
  to: context.contractAddress,
4707
5189
  value: 0n,
4708
5190
  data: MORPHO_AUTHORIZATION_INTERFACE.encodeFunctionData("setAuthorization", [
4709
5191
  context.authorized,
4710
5192
  false,
4711
5193
  ]),
5194
+ },
4712
5195
  });
4713
5196
  cleanup.authorizations.push({
4714
5197
  contractAddress: context.contractAddress,
@@ -5179,12 +5662,19 @@ export class WdkEvmWalletService {
5179
5662
  };
5180
5663
  }
5181
5664
 
5182
- async #uniswapTradingApiRequest(pathname, body) {
5665
+ async #uniswapTradingApiRequest(
5666
+ pathname,
5667
+ body,
5668
+ { erc20EthEnabled = false, routerVersion = this.config.uniswapRouterVersion, chainId = null } = {}
5669
+ ) {
5183
5670
  const headers = {
5184
5671
  "Content-Type": "application/json",
5185
5672
  Accept: "application/json",
5186
- "x-universal-router-version": this.config.uniswapRouterVersion,
5673
+ "x-universal-router-version": routerVersion,
5187
5674
  };
5675
+ if (erc20EthEnabled) {
5676
+ headers["x-erc20eth-enabled"] = "true";
5677
+ }
5188
5678
  if (this.config.uniswapViaGateway) {
5189
5679
  // The provider gateway holds the Uniswap key and injects x-api-key upstream;
5190
5680
  // we authenticate to it with the shared gateway bearer (same token used for
@@ -5193,6 +5683,13 @@ export class WdkEvmWalletService {
5193
5683
  if (token) {
5194
5684
  headers.Authorization = `Bearer ${token}`;
5195
5685
  }
5686
+ // The gateway selects its own configured version and checks this value for
5687
+ // equality. The chain id is an allow-listed routing hint, never an upstream
5688
+ // header passthrough.
5689
+ if (chainId !== null) {
5690
+ headers["x-agentlayer-chain-id"] = String(chainId);
5691
+ headers["x-agentlayer-uniswap-router-version"] = routerVersion;
5692
+ }
5196
5693
  } else {
5197
5694
  if (!this.config.uniswapApiKey) {
5198
5695
  throw createTaggedError(
@@ -5246,9 +5743,9 @@ export class WdkEvmWalletService {
5246
5743
  return payload;
5247
5744
  }
5248
5745
 
5249
- async #fetchUniswapQuote({ runtimeConfig, address, swapRequest }) {
5746
+ async #fetchUniswapQuote({ runtimeConfig, routerProfile, address, swapRequest }) {
5250
5747
  const chainId = UNISWAP_SUPPORTED_CHAIN_IDS[runtimeConfig.network];
5251
- const payload = await this.#uniswapTradingApiRequest("/quote", {
5748
+ const quoteRequest = {
5252
5749
  swapper: address,
5253
5750
  tokenIn: swapRequest.tokenIn,
5254
5751
  tokenOut: swapRequest.tokenOut,
@@ -5257,21 +5754,50 @@ export class WdkEvmWalletService {
5257
5754
  amount: swapRequest.tokenInAmount.toString(),
5258
5755
  type: "EXACT_INPUT",
5259
5756
  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
- });
5757
+ };
5758
+ const requestOptions = {
5759
+ erc20EthEnabled: isZeroAddress(swapRequest.tokenIn),
5760
+ routerVersion: routerProfile.routerVersion,
5761
+ chainId,
5762
+ };
5763
+ const ammProtocols = routerProfile.ammProtocols;
5764
+ let payload;
5765
+ try {
5766
+ // Prefer the full route set. On a sub-minimum UniswapX request the API can
5767
+ // return No quotes available even when an AMM pool is liquid, so retry the
5768
+ // same exact swap through AMMs only before reporting a quote failure.
5769
+ payload = await this.#uniswapTradingApiRequest(
5770
+ "/quote",
5771
+ {
5772
+ ...quoteRequest,
5773
+ protocols: [
5774
+ ...ammProtocols,
5775
+ runtimeConfig.network === "ethereum" ? "UNISWAPX_V2" : "UNISWAPX_V3",
5776
+ ],
5777
+ },
5778
+ requestOptions
5779
+ );
5780
+ } catch (error) {
5781
+ if (!isUniswapNoQuotesAvailableError(error)) {
5782
+ throw error;
5783
+ }
5784
+ payload = await this.#uniswapTradingApiRequest(
5785
+ "/quote",
5786
+ { ...quoteRequest, protocols: ammProtocols },
5787
+ requestOptions
5788
+ );
5789
+ }
5264
5790
  const routing = String(payload.routing || "").toUpperCase();
5265
- if (routing !== "CLASSIC") {
5791
+ if (!UNISWAP_CLASSIC_ROUTINGS.has(routing) && !UNISWAPX_ROUTINGS.has(routing)) {
5266
5792
  throw createTaggedError(
5267
- `Uniswap returned unsupported routing '${routing}'. Only CLASSIC is enabled in this runtime.`,
5793
+ `Uniswap returned unsupported routing '${routing}'.`,
5268
5794
  "uniswap_unsupported_route",
5269
5795
  { provider: "uniswap", routing }
5270
5796
  );
5271
5797
  }
5272
5798
  if (!payload.quote || typeof payload.quote !== "object" || !payload.quote.output) {
5273
5799
  throw createTaggedError(
5274
- "Uniswap quote response is missing CLASSIC quote/output fields.",
5800
+ "Uniswap quote response is missing quote/output fields.",
5275
5801
  "network_unavailable",
5276
5802
  { provider: "uniswap" }
5277
5803
  );
@@ -5279,8 +5805,195 @@ export class WdkEvmWalletService {
5279
5805
  return payload;
5280
5806
  }
5281
5807
 
5808
+ async #quoteRobinhoodV3Direct({ runtimeConfig, networkProfile, swapRequest }) {
5809
+ const fallback = networkProfile.v3DirectFallback;
5810
+ if (!fallback) {
5811
+ return null;
5812
+ }
5813
+ const tokenIn = isZeroAddress(swapRequest.tokenIn)
5814
+ ? networkProfile.wrappedNative
5815
+ : swapRequest.tokenIn;
5816
+ const tokenOut = isZeroAddress(swapRequest.tokenOut)
5817
+ ? networkProfile.wrappedNative
5818
+ : swapRequest.tokenOut;
5819
+ let best = null;
5820
+ for (const feeTier of fallback.feeTiers) {
5821
+ const data = UNISWAP_V3_QUOTER_INTERFACE.encodeFunctionData("quoteExactInputSingle", [
5822
+ {
5823
+ tokenIn,
5824
+ tokenOut,
5825
+ amountIn: swapRequest.tokenInAmount,
5826
+ fee: feeTier,
5827
+ sqrtPriceLimitX96: 0,
5828
+ },
5829
+ ]);
5830
+ try {
5831
+ const raw = await rpcRequest(runtimeConfig.providerUrl, "eth_call", [
5832
+ { to: fallback.quoterV2, data },
5833
+ "latest",
5834
+ ]);
5835
+ const decoded = UNISWAP_V3_QUOTER_INTERFACE.decodeFunctionResult(
5836
+ "quoteExactInputSingle",
5837
+ raw
5838
+ );
5839
+ const amountOut = BigInt(decoded[0]);
5840
+ if (amountOut > 0n && (!best || amountOut > best.amountOut)) {
5841
+ best = { feeTier, amountOut, gasEstimate: BigInt(decoded[3]) };
5842
+ }
5843
+ } catch {
5844
+ // A missing pool and a failed Quoter call are equivalent for this
5845
+ // constrained direct route. Try every reviewed fee tier before failing.
5846
+ }
5847
+ }
5848
+ return best;
5849
+ }
5850
+
5851
+ async #buildRobinhoodV3DirectFallbackPlan({ account, runtimeConfig, address, swapRequest }) {
5852
+ const networkProfile = getUniswapNetworkExecutionProfile(runtimeConfig.network);
5853
+ const quoted = await this.#quoteRobinhoodV3Direct({
5854
+ runtimeConfig,
5855
+ networkProfile,
5856
+ swapRequest,
5857
+ });
5858
+ if (!quoted) {
5859
+ return null;
5860
+ }
5861
+ const slippageBps = Math.round(swapRequest.slippagePercent * 100);
5862
+ const minimumTokenOutAmount =
5863
+ quoted.amountOut - (quoted.amountOut * BigInt(slippageBps)) / 10000n;
5864
+ const nativeTokenIn = isZeroAddress(swapRequest.tokenIn);
5865
+ const spender = nativeTokenIn ? null : networkProfile.v3DirectFallback.swapRouter02;
5866
+ const allowanceState = nativeTokenIn
5867
+ ? { currentAllowance: swapRequest.tokenInAmount, error: null }
5868
+ : await this.#getSwapAllowanceState({
5869
+ account,
5870
+ tokenAddress: swapRequest.tokenIn,
5871
+ spender,
5872
+ });
5873
+ const approval = nativeTokenIn
5874
+ ? { required: false, estimatedFee: 0n, steps: [] }
5875
+ : await this.#buildSwapApprovalPlan({
5876
+ account,
5877
+ runtimeConfig,
5878
+ tokenAddress: swapRequest.tokenIn,
5879
+ spender,
5880
+ requiredAmount: swapRequest.tokenInAmount,
5881
+ currentAllowance: allowanceState.currentAllowance,
5882
+ });
5883
+ const preparedTransaction = buildUniswapV3DirectSwapTransaction({
5884
+ networkProfile,
5885
+ recipient: address,
5886
+ swapRequest,
5887
+ feeTier: quoted.feeTier,
5888
+ minimumTokenOutAmount,
5889
+ });
5890
+ const quoteFingerprint = sha256Hex(
5891
+ JSON.stringify({
5892
+ chainId: runtimeConfig.chainId,
5893
+ network: runtimeConfig.network,
5894
+ from: address.toLowerCase(),
5895
+ router: networkProfile.v3DirectFallback.swapRouter02,
5896
+ spender: spender ? spender.toLowerCase() : null,
5897
+ tokenIn: swapRequest.tokenIn.toLowerCase(),
5898
+ tokenOut: swapRequest.tokenOut.toLowerCase(),
5899
+ tokenInAmount: swapRequest.tokenInAmount.toString(),
5900
+ feeTier: quoted.feeTier,
5901
+ slippageBps,
5902
+ executionKind: "v3-direct",
5903
+ })
5904
+ );
5905
+ return {
5906
+ quoteResponse: null,
5907
+ permitData: null,
5908
+ isNativeTokenIn: nativeTokenIn,
5909
+ spender,
5910
+ currentAllowance: allowanceState.currentAllowance,
5911
+ allowanceReadError: allowanceState.error,
5912
+ approval,
5913
+ tokenInAmount: swapRequest.tokenInAmount,
5914
+ tokenOutAmount: quoted.amountOut,
5915
+ minimumTokenOutAmount,
5916
+ slippageBps,
5917
+ routing: "CLASSIC",
5918
+ executionKind: "v3-direct",
5919
+ isUniswapX: false,
5920
+ quoteFingerprint,
5921
+ // QuoterV2 returns a gas-unit estimate, not a wei-denominated fee.
5922
+ // Keep the fee field empty rather than mislabeling units in the preview.
5923
+ gasFee: null,
5924
+ gasFeeUSD: null,
5925
+ router: networkProfile.v3DirectFallback.swapRouter02,
5926
+ routerProfile: null,
5927
+ preparedTransaction,
5928
+ v3FeeTier: quoted.feeTier,
5929
+ };
5930
+ }
5931
+
5282
5932
  async #buildUniswapSwapPlan({ account, runtimeConfig, address, swapRequest }) {
5283
- const quoteResponse = await this.#fetchUniswapQuote({ runtimeConfig, address, swapRequest });
5933
+ const networkProfile = getUniswapNetworkExecutionProfile(runtimeConfig.network);
5934
+ const directOperation = buildDirectWrappedNativeOperation(networkProfile, swapRequest);
5935
+ const slippageBps = Math.round(swapRequest.slippagePercent * 100);
5936
+ if (directOperation) {
5937
+ const quoteFingerprint = sha256Hex(
5938
+ JSON.stringify({
5939
+ chainId: runtimeConfig.chainId,
5940
+ network: runtimeConfig.network,
5941
+ from: address.toLowerCase(),
5942
+ executionKind: directOperation.executionKind,
5943
+ wrappedNative: networkProfile.wrappedNative,
5944
+ tokenIn: swapRequest.tokenIn,
5945
+ tokenOut: swapRequest.tokenOut,
5946
+ tokenInAmount: swapRequest.tokenInAmount.toString(),
5947
+ })
5948
+ );
5949
+ return {
5950
+ quoteResponse: null,
5951
+ permitData: null,
5952
+ isNativeTokenIn: isZeroAddress(swapRequest.tokenIn),
5953
+ spender: null,
5954
+ currentAllowance: 0n,
5955
+ allowanceReadError: null,
5956
+ approval: { required: false, estimatedFee: 0n, steps: [] },
5957
+ tokenInAmount: swapRequest.tokenInAmount,
5958
+ tokenOutAmount: swapRequest.tokenInAmount,
5959
+ minimumTokenOutAmount: swapRequest.tokenInAmount,
5960
+ slippageBps,
5961
+ routing: directOperation.routing,
5962
+ executionKind: directOperation.executionKind,
5963
+ isUniswapX: false,
5964
+ quoteFingerprint,
5965
+ gasFee: null,
5966
+ gasFeeUSD: null,
5967
+ router: networkProfile.wrappedNative,
5968
+ routerProfile: null,
5969
+ preparedTransaction: directOperation.transaction,
5970
+ };
5971
+ }
5972
+
5973
+ const routerProfile = this.#resolveUniswapRouterProfile(runtimeConfig);
5974
+ let quoteResponse;
5975
+ try {
5976
+ quoteResponse = await this.#fetchUniswapQuote({
5977
+ runtimeConfig,
5978
+ routerProfile,
5979
+ address,
5980
+ swapRequest,
5981
+ });
5982
+ } catch (error) {
5983
+ if (runtimeConfig.network !== "robinhood" || !isUniswapNoQuotesAvailableError(error)) {
5984
+ throw error;
5985
+ }
5986
+ const fallbackPlan = await this.#buildRobinhoodV3DirectFallbackPlan({
5987
+ account,
5988
+ runtimeConfig,
5989
+ address,
5990
+ swapRequest,
5991
+ });
5992
+ if (fallbackPlan) {
5993
+ return fallbackPlan;
5994
+ }
5995
+ throw error;
5996
+ }
5284
5997
  const permitData = quoteResponse.permitData ?? null;
5285
5998
  const isNativeTokenIn = isZeroAddress(swapRequest.tokenIn);
5286
5999
  const spender = isNativeTokenIn ? null : PERMIT2_ADDRESS;
@@ -5302,16 +6015,17 @@ export class WdkEvmWalletService {
5302
6015
  requiredAmount: swapRequest.tokenInAmount,
5303
6016
  currentAllowance,
5304
6017
  });
5305
- const tokenOutAmount = BigInt(String(quoteResponse.quote.output.amount || "0"));
5306
- const slippageBps = Math.round(swapRequest.slippagePercent * 100);
6018
+ const quoteOutput = quoteResponse.quote.output;
6019
+ const tokenOutAmount = BigInt(String(quoteOutput.amount || "0"));
5307
6020
  // The Trading API returns the post-slippage floor directly; fall back to a
5308
6021
  // local computation only if the field is absent.
5309
6022
  const minimumTokenOutAmount =
5310
- quoteResponse.quote.output.minimumAmount !== undefined &&
5311
- quoteResponse.quote.output.minimumAmount !== null
5312
- ? BigInt(String(quoteResponse.quote.output.minimumAmount))
6023
+ quoteOutput.minimumAmount !== undefined && quoteOutput.minimumAmount !== null
6024
+ ? BigInt(String(quoteOutput.minimumAmount))
5313
6025
  : tokenOutAmount - (tokenOutAmount * BigInt(slippageBps)) / 10000n;
5314
- const router = UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK[runtimeConfig.network];
6026
+ const routing = String(quoteResponse.routing || "").toUpperCase();
6027
+ const isUniswapX = UNISWAPX_ROUTINGS.has(routing);
6028
+ const router = isUniswapX ? null : routerProfile.router;
5315
6029
  // Bind only the stable swap *intent* (who/what/how-much/slippage/route), never
5316
6030
  // the live quoted output: the Trading API re-prices every block, so including
5317
6031
  // tokenOutAmount here made execute's re-quote fingerprint differ from preview's
@@ -5323,13 +6037,15 @@ export class WdkEvmWalletService {
5323
6037
  chainId: runtimeConfig.chainId,
5324
6038
  network: runtimeConfig.network,
5325
6039
  from: address.toLowerCase(),
5326
- router: router.toLowerCase(),
6040
+ router: router ? router.toLowerCase() : null,
5327
6041
  spender: spender ? spender.toLowerCase() : null,
5328
6042
  tokenIn: swapRequest.tokenIn.toLowerCase(),
5329
6043
  tokenOut: swapRequest.tokenOut.toLowerCase(),
5330
6044
  tokenInAmount: swapRequest.tokenInAmount.toString(),
5331
6045
  slippageBps,
5332
- routing: "CLASSIC",
6046
+ routing,
6047
+ executionKind: isUniswapX ? "uniswapx" : "classic",
6048
+ routerVersion: routerProfile.routerVersion,
5333
6049
  })
5334
6050
  );
5335
6051
  return {
@@ -5344,13 +6060,25 @@ export class WdkEvmWalletService {
5344
6060
  tokenOutAmount,
5345
6061
  minimumTokenOutAmount,
5346
6062
  slippageBps,
6063
+ routing,
6064
+ executionKind: isUniswapX ? "uniswapx" : "classic",
6065
+ isUniswapX,
5347
6066
  quoteFingerprint,
5348
6067
  gasFee: quoteResponse.quote.gasFee ?? null,
5349
6068
  gasFeeUSD: quoteResponse.quote.gasFeeUSD ?? null,
5350
6069
  router,
6070
+ routerProfile,
6071
+ preparedTransaction: null,
5351
6072
  };
5352
6073
  }
5353
6074
 
6075
+ #resolveUniswapRouterProfile(runtimeConfig) {
6076
+ const configuredVersion =
6077
+ this.config.uniswapRouterVersionsByNetwork?.[runtimeConfig.network] ||
6078
+ this.config.uniswapRouterVersion;
6079
+ return resolveUniswapRouterExecutionProfile(runtimeConfig.network, configuredVersion);
6080
+ }
6081
+
5354
6082
  async #signUniswapPermit(account, permitData, runtimeConfig) {
5355
6083
  const typed = normalizeUniswapPermitData(permitData, runtimeConfig);
5356
6084
  return account.signTypedData({
@@ -5360,7 +6088,13 @@ export class WdkEvmWalletService {
5360
6088
  });
5361
6089
  }
5362
6090
 
5363
- async #fetchUniswapSwapCalldata({ runtimeConfig, quoteResponse, permitData, signature }) {
6091
+ async #fetchUniswapSwapCalldata({
6092
+ runtimeConfig,
6093
+ routerProfile,
6094
+ quoteResponse,
6095
+ permitData,
6096
+ signature,
6097
+ }) {
5364
6098
  const { permitData: _permitData, permitTransaction: _permitTransaction, ...cleanQuote } =
5365
6099
  quoteResponse;
5366
6100
  const body = { ...cleanQuote };
@@ -5370,10 +6104,13 @@ export class WdkEvmWalletService {
5370
6104
  body.signature = signature;
5371
6105
  body.permitData = permitData;
5372
6106
  }
5373
- const payload = await this.#uniswapTradingApiRequest("/swap", body);
6107
+ const payload = await this.#uniswapTradingApiRequest("/swap", body, {
6108
+ routerVersion: routerProfile.routerVersion,
6109
+ chainId: runtimeConfig.chainId,
6110
+ });
5374
6111
  const swap = payload.swap || {};
5375
6112
  const to = normalizeAddress(String(swap.to || ""), "swap.to");
5376
- const expectedRouter = UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK[runtimeConfig.network];
6113
+ const expectedRouter = routerProfile.router;
5377
6114
  if (to.toLowerCase() !== expectedRouter) {
5378
6115
  throw createTaggedError(
5379
6116
  "Uniswap /swap returned an unexpected target contract.",
@@ -5382,6 +6119,7 @@ export class WdkEvmWalletService {
5382
6119
  );
5383
6120
  }
5384
6121
  const data = assertNonEmptyString(String(swap.data || ""), "swap.data");
6122
+ const value = parseHexOrDecimalBigInt(swap.value || "0", "swap.value");
5385
6123
  if (data === "0x") {
5386
6124
  throw createTaggedError(
5387
6125
  "Uniswap /swap returned empty calldata. The quote likely expired; generate a new preview.",
@@ -5392,7 +6130,38 @@ export class WdkEvmWalletService {
5392
6130
  return {
5393
6131
  to,
5394
6132
  data,
5395
- value: parseHexOrDecimalBigInt(swap.value || "0", "swap.value"),
6133
+ value,
6134
+ };
6135
+ }
6136
+
6137
+ async #submitUniswapOrder({ quoteResponse, signature, nativeTokenIn, routing, routerProfile }) {
6138
+ if (!signature) {
6139
+ throw createTaggedError(
6140
+ "UniswapX order quote is missing required signature data.",
6141
+ "uniswap_order_signature_missing",
6142
+ { provider: "uniswap" }
6143
+ );
6144
+ }
6145
+ const payload = await this.#uniswapTradingApiRequest(
6146
+ "/order",
6147
+ { quote: quoteResponse.quote, routing, signature },
6148
+ {
6149
+ erc20EthEnabled: nativeTokenIn,
6150
+ routerVersion: routerProfile.routerVersion,
6151
+ chainId: routerProfile.chainId,
6152
+ }
6153
+ );
6154
+ const orderId = String(payload.orderId || payload.order?.orderId || "").trim();
6155
+ if (!orderId) {
6156
+ throw createTaggedError(
6157
+ "Uniswap /order response is missing orderId.",
6158
+ "network_unavailable",
6159
+ { provider: "uniswap" }
6160
+ );
6161
+ }
6162
+ return {
6163
+ orderId,
6164
+ orderStatus: String(payload.orderStatus || payload.order?.orderStatus || "open"),
5396
6165
  };
5397
6166
  }
5398
6167
 
@@ -7533,13 +8302,21 @@ export class WdkEvmWalletService {
7533
8302
  }
7534
8303
  }
7535
8304
 
7536
- async #waitForTransactionReceipt(runtimeConfig, txHash) {
8305
+ async #waitForTransactionReceipt(
8306
+ runtimeConfig,
8307
+ txHash,
8308
+ {
8309
+ operationLabel = "Approval transaction",
8310
+ failureCode = "swap_approval_failed",
8311
+ timeoutCode = "swap_approval_timeout",
8312
+ } = {}
8313
+ ) {
7537
8314
  for (let attempt = 0; attempt < 30; attempt += 1) {
7538
8315
  const receipt = await rpcRequest(runtimeConfig.providerUrl, "eth_getTransactionReceipt", [txHash]);
7539
8316
  if (receipt) {
7540
8317
  const status = String(receipt.status || "").toLowerCase();
7541
8318
  if (status === "0x0") {
7542
- throw createTaggedError("Approval transaction reverted onchain.", "swap_approval_failed", {
8319
+ throw createTaggedError(`${operationLabel} reverted onchain.`, failureCode, {
7543
8320
  txHash,
7544
8321
  network: runtimeConfig.network,
7545
8322
  });
@@ -7549,8 +8326,8 @@ export class WdkEvmWalletService {
7549
8326
  await new Promise((resolve) => setTimeout(resolve, 1000));
7550
8327
  }
7551
8328
  throw createTaggedError(
7552
- "Timed out waiting for approval transaction confirmation.",
7553
- "swap_approval_timeout",
8329
+ `Timed out waiting for ${operationLabel.toLowerCase()} confirmation.`,
8330
+ timeoutCode,
7554
8331
  {
7555
8332
  txHash,
7556
8333
  network: runtimeConfig.network,
@@ -7560,11 +8337,17 @@ export class WdkEvmWalletService {
7560
8337
  }
7561
8338
 
7562
8339
  export const __testables = {
8340
+ applyGasBuffer,
8341
+ MORPHO_GAS_BUFFER_BPS,
7563
8342
  PERMIT2_ADDRESS,
7564
8343
  UNISWAP_SUPPORTED_CHAIN_IDS,
8344
+ UNISWAP_EXECUTION_PROFILES,
7565
8345
  UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK,
8346
+ UNISWAP_WRAPPED_NATIVE_BY_NETWORK,
7566
8347
  normalizeUniswapTokenAddress,
7567
8348
  assertUniswapSupportedNetwork,
8349
+ resolveUniswapRouterExecutionProfile,
8350
+ buildDirectWrappedNativeOperation,
7568
8351
  assertValidNetwork,
7569
8352
  uniswapSlippagePercentFromBps,
7570
8353
  normalizeUniswapPermitData,