@uniswap/universal-router-sdk 4.29.5 → 4.31.0

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/README.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # universal-router-sdk
2
+
2
3
  This SDK facilitates interactions with the contracts in [Universal Router](https://github.com/Uniswap/universal-router)
3
4
 
4
5
  ## Usage
6
+
5
7
  Install latest version of universal-router-sdk. Then import the corresponding Trade class and Data object for each protocol you'd like to interact with.
6
8
 
7
9
  ### Trading on Uniswap
10
+
8
11
  warning: `swapERC20CallParameters()` to be deprecated in favor of `swapCallParameters()`
12
+
9
13
  ```typescript
10
14
  import { TradeType } from '@uniswap/sdk-core'
11
15
  import { Trade as V2TradeSDK } from '@uniswap/v2-sdk'
@@ -19,15 +23,302 @@ const { calldata, value } = SwapRouter.swapCallParameters(routerTrade, options)
19
23
  ```
20
24
 
21
25
  ## Running this package
26
+
22
27
  Make sure you are running `node v18`
23
28
  Install dependencies and run typescript unit tests
29
+
24
30
  ```bash
25
31
  yarn install
26
32
  yarn test:hardhat
27
33
  ```
28
34
 
29
35
  Run forge integration tests
36
+
30
37
  ```bash
31
38
  forge install
32
39
  yarn test:forge
33
40
  ```
41
+
42
+ ## Per-Hop Slippage Protection (V4 Routes)
43
+
44
+ Universal Router v2.1 adds granular slippage protection for multi-hop V4 swaps. Additionally to checking slippage at the end of a route, you can now verify that each individual hop doesn't exceed a maximum price limit.
45
+
46
+ ### How It Works
47
+
48
+ For V4 multi-hop swaps, you can provide a `maxHopSlippage` array in your swap options:
49
+
50
+ ```typescript
51
+ import { SwapRouter } from '@uniswap/universal-router-sdk'
52
+ import { BigNumber } from 'ethers'
53
+ import { Percent } from '@uniswap/sdk-core'
54
+
55
+ const swapOptions = {
56
+ slippageTolerance: new Percent(50, 10000), // 0.5% overall slippage
57
+ recipient: '0x...',
58
+ deadline: Math.floor(Date.now() / 1000) + 60 * 20,
59
+ // Optional: per-hop slippage protection for V4 routes
60
+ maxHopSlippage: [
61
+ BigNumber.from('1010000000000000000'), // Hop 0: max price 1.01 (1% slippage)
62
+ BigNumber.from('2500000000000000000000'), // Hop 1: max price 2500
63
+ ]
64
+ }
65
+
66
+ const { calldata, value } = SwapRouter.swapCallParameters(trade, swapOptions)
67
+ ```
68
+
69
+ ### Price Calculation
70
+
71
+ The slippage is expressed as a **price** with 18 decimals of precision:
72
+
73
+ - **For Exact Input**: `price = amountIn * 1e18 / amountOut`
74
+ - **For Exact Output**: `price = amountIn * 1e18 / amountOut`
75
+
76
+ If the calculated price exceeds `maxHopSlippage[i]`, the transaction will revert with:
77
+
78
+ - `V4TooLittleReceivedPerHop` for exact input swaps
79
+ - `V4TooMuchRequestedPerHop` for exact output swaps
80
+
81
+ ### Example: USDC → DAI → WETH
82
+
83
+ ```typescript
84
+ // 2-hop swap: USDC → DAI → WETH
85
+ const swapOptions = {
86
+ slippageTolerance: new Percent(100, 10000), // 1% overall
87
+ recipient: userAddress,
88
+ deadline,
89
+ maxHopSlippage: [
90
+ BigNumber.from('1010000000000000000'), // Hop 0: USDC→DAI, max 1% slippage
91
+ BigNumber.from('2500000000000000000000'), // Hop 1: DAI→WETH, max price 2500 DAI/WETH
92
+ ]
93
+ }
94
+ ```
95
+
96
+ ### Benefits
97
+
98
+ 1. **MEV Protection**: Prevents sandwich attacks on individual hops
99
+ 2. **Route Quality**: Ensures each segment of a multi-hop route meets expectations
100
+ 3. **Granular Control**: Different slippage tolerance for different pairs in a route
101
+
102
+ ### Backward Compatibility
103
+
104
+ - If `maxHopSlippage` is not provided or is an empty array, only overall slippage is checked (backward compatible)
105
+ - The feature only applies to V4 routes; V2 and V3 routes ignore this parameter
106
+ - Mixed routes with V4 sections will apply per-hop checks only to the V4 portions
107
+
108
+ ## Signed Routes (Universal Router v2.1)
109
+
110
+ Universal Router v2.1 supports EIP712-signed route execution, enabling gasless transactions and intent-based trading.
111
+
112
+ **Important**: The SDK does not perform signing. It provides utilities to prepare EIP712 payloads and encode signed calldata. You sign with your own mechanism (wallet, KMS, hardware, etc.).
113
+
114
+ ### Basic Flow
115
+
116
+ ```typescript
117
+ import { SwapRouter, NONCE_SKIP_CHECK } from '@uniswap/universal-router-sdk'
118
+ import { Wallet } from '@ethersproject/wallet'
119
+
120
+ const wallet = new Wallet('0x...')
121
+ const chainId = 1
122
+ const routerAddress = '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD'
123
+ const deadline = Math.floor(Date.now() / 1000) + 60 * 20
124
+
125
+ // 1. Generate regular swap calldata
126
+ const { calldata, value } = SwapRouter.swapCallParameters(trade, {
127
+ slippageTolerance: new Percent(50, 10000),
128
+ recipient: wallet.address,
129
+ deadline,
130
+ })
131
+
132
+ // 2. Get EIP712 payload to sign
133
+ const payload = SwapRouter.getExecuteSignedPayload(
134
+ calldata,
135
+ {
136
+ intent: '0x' + '0'.repeat(64), // Application-specific intent
137
+ data: '0x' + '0'.repeat(64), // Application-specific data
138
+ sender: wallet.address, // Or address(0) to skip sender verification
139
+ },
140
+ deadline,
141
+ chainId,
142
+ routerAddress
143
+ )
144
+
145
+ // 3. Sign externally (wallet/KMS/hardware)
146
+ const signature = await wallet._signTypedData(payload.domain, payload.types, payload.value)
147
+
148
+ // 4. Encode for executeSigned()
149
+ const { calldata: signedCalldata, value: signedValue } = SwapRouter.encodeExecuteSigned(
150
+ calldata,
151
+ signature,
152
+ {
153
+ intent: payload.value.intent,
154
+ data: payload.value.data,
155
+ sender: payload.value.sender,
156
+ nonce: payload.value.nonce, // Must match what was signed
157
+ },
158
+ deadline,
159
+ BigNumber.from(value)
160
+ )
161
+
162
+ // 5. Submit transaction
163
+ await wallet.sendTransaction({
164
+ to: routerAddress,
165
+ data: signedCalldata,
166
+ value: signedValue,
167
+ })
168
+ ```
169
+
170
+ ### Nonce Management
171
+
172
+ - **Random nonce (default)**: Omit `nonce` parameter - SDK generates random nonce
173
+ - **Skip nonce check**: Use `NONCE_SKIP_CHECK` sentinel to allow signature reuse
174
+ - **Custom nonce**: Provide your own nonce for ordering
175
+
176
+ ```typescript
177
+ import { NONCE_SKIP_CHECK } from '@uniswap/universal-router-sdk'
178
+
179
+ // Reusable signature (no nonce check)
180
+ const payload = SwapRouter.getExecuteSignedPayload(
181
+ calldata,
182
+ {
183
+ intent: '0x...',
184
+ data: '0x...',
185
+ sender: '0x0000000000000000000000000000000000000000', // Skip sender verification too
186
+ nonce: NONCE_SKIP_CHECK, // Allow signature reuse
187
+ },
188
+ deadline,
189
+ chainId,
190
+ routerAddress
191
+ )
192
+ ```
193
+
194
+ ### Sender Verification
195
+
196
+ - **Verify sender**: Pass the actual sender address (e.g., `wallet.address`)
197
+ - **Skip verification**: Pass `'0x0000000000000000000000000000000000000000'`
198
+
199
+ The SDK automatically sets `verifySender` based on whether sender is address(0).
200
+
201
+ ## Cross-Chain Bridging with Across (Universal Router v2.1)
202
+
203
+ Universal Router v2.1 integrates with Across Protocol V3 to enable seamless cross-chain bridging after swaps. This allows you to swap tokens on one chain and automatically bridge them to another chain in a single transaction.
204
+
205
+ ### Basic Usage
206
+
207
+ ```typescript
208
+ import { SwapRouter } from '@uniswap/universal-router-sdk'
209
+ import { BigNumber } from 'ethers'
210
+
211
+ // 1. Prepare your swap (e.g., USDC → WETH on mainnet)
212
+ const { calldata, value } = SwapRouter.swapCallParameters(
213
+ trade,
214
+ swapOptions,
215
+ [
216
+ {
217
+ // Bridge configuration
218
+ depositor: userAddress,
219
+ recipient: userAddress, // Recipient on destination chain
220
+ inputToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH mainnet
221
+ outputToken: '0x4200000000000000000000000000000000000006', // WETH optimism
222
+ inputAmount: BigNumber.from('1000000000000000000'), // 1 WETH
223
+ outputAmount: BigNumber.from('990000000000000000'), // 0.99 WETH (with fees)
224
+ destinationChainId: 10, // Optimism
225
+ exclusiveRelayer: '0x0000000000000000000000000000000000000000',
226
+ quoteTimestamp: Math.floor(Date.now() / 1000),
227
+ fillDeadline: Math.floor(Date.now() / 1000) + 3600,
228
+ exclusivityDeadline: 0,
229
+ message: '0x',
230
+ useNative: false,
231
+ }
232
+ ]
233
+ )
234
+ ```
235
+
236
+ ### Swap + Bridge Example
237
+
238
+ ```typescript
239
+ // Swap USDC to WETH, then bridge WETH to Optimism
240
+ const bridgeParams = {
241
+ depositor: userAddress,
242
+ recipient: userAddress, // Can be different address on destination
243
+ inputToken: WETH_MAINNET,
244
+ outputToken: WETH_OPTIMISM,
245
+ inputAmount: CONTRACT_BALANCE, // Use entire swap output
246
+ outputAmount: expectedOutputAmount,
247
+ destinationChainId: 10,
248
+ exclusiveRelayer: '0x0000000000000000000000000000000000000000',
249
+ quoteTimestamp: Math.floor(Date.now() / 1000),
250
+ fillDeadline: Math.floor(Date.now() / 1000) + 3600,
251
+ exclusivityDeadline: 0,
252
+ message: '0x', // Optional message to execute on destination
253
+ useNative: false, // Set to true to bridge native ETH
254
+ }
255
+
256
+ const { calldata, value } = SwapRouter.swapCallParameters(
257
+ trade,
258
+ swapOptions,
259
+ [bridgeParams] // Array of bridge operations
260
+ )
261
+ ```
262
+
263
+ ### Using CONTRACT_BALANCE
264
+
265
+ When bridging after a swap, you often don't know the exact output amount. Use `CONTRACT_BALANCE` to bridge the entire contract balance:
266
+
267
+ ```typescript
268
+ import { CONTRACT_BALANCE } from '@uniswap/universal-router-sdk'
269
+
270
+ const bridgeParams = {
271
+ // ... other params
272
+ inputAmount: CONTRACT_BALANCE, // Bridge entire balance after swap
273
+ // ... other params
274
+ }
275
+ ```
276
+
277
+ ### Multiple Bridge Operations
278
+
279
+ You can perform multiple bridge operations after a swap:
280
+
281
+ ```typescript
282
+ const { calldata, value } = SwapRouter.swapCallParameters(
283
+ trade,
284
+ swapOptions,
285
+ [
286
+ {
287
+ // Bridge 50% to Optimism
288
+ inputToken: WETH_MAINNET,
289
+ outputToken: WETH_OPTIMISM,
290
+ inputAmount: BigNumber.from('500000000000000000'),
291
+ destinationChainId: 10,
292
+ // ... other params
293
+ },
294
+ {
295
+ // Bridge remaining USDC to Arbitrum
296
+ inputToken: USDC_MAINNET,
297
+ outputToken: USDC_ARBITRUM,
298
+ inputAmount: CONTRACT_BALANCE,
299
+ destinationChainId: 42161,
300
+ // ... other params
301
+ }
302
+ ]
303
+ )
304
+ ```
305
+
306
+ ### Native ETH Bridging
307
+
308
+ To bridge native ETH instead of WETH:
309
+
310
+ ```typescript
311
+ const bridgeParams = {
312
+ inputToken: WETH_ADDRESS, // Must be WETH address
313
+ outputToken: WETH_ON_DESTINATION,
314
+ useNative: true, // Bridge as native ETH
315
+ // ... other params
316
+ }
317
+ ```
318
+
319
+ ### Important Notes
320
+
321
+ 1. **Across Quote**: Bridge parameters (especially `outputAmount`, `quoteTimestamp`, `fillDeadline`) should come from the Across API quote
322
+ 2. **Recipient Address**: Can be different from the sender, allowing cross-chain transfers to other addresses
323
+ 3. **Message Passing**: The `message` field allows executing arbitrary calls on the destination chain
324
+ 4. **Slippage**: The `outputAmount` already accounts for bridge fees and slippage
@@ -0,0 +1,21 @@
1
+ import { BigNumberish } from 'ethers';
2
+ /**
3
+ * Parameters for Across V4 Deposit V3 command
4
+ * Used for cross-chain bridging via Across Protocol V3 SpokePool
5
+ */
6
+ export declare type AcrossV4DepositV3Params = {
7
+ depositor: string;
8
+ recipient: string;
9
+ inputToken: string;
10
+ outputToken: string;
11
+ inputAmount: BigNumberish;
12
+ outputAmount: BigNumberish;
13
+ destinationChainId: number;
14
+ exclusiveRelayer: string;
15
+ quoteTimestamp: number;
16
+ fillDeadline: number;
17
+ exclusivityDeadline: number;
18
+ message: string;
19
+ useNative: boolean;
20
+ };
21
+ export { CONTRACT_BALANCE } from '../../utils/constants';
@@ -1,2 +1,3 @@
1
1
  export * from './uniswap';
2
2
  export * from './unwrapWETH';
3
+ export * from './across';
@@ -1,9 +1,10 @@
1
1
  import { RoutePlanner } from '../../utils/routerCommands';
2
+ import { URVersion } from '@uniswap/v4-sdk';
2
3
  import { Trade as RouterTrade, SwapOptions as RouterSwapOptions } from '@uniswap/router-sdk';
3
4
  import { Permit2Permit } from '../../utils/inputTokens';
4
5
  import { Currency, TradeType } from '@uniswap/sdk-core';
5
6
  import { Command, RouterActionType, TradeConfig } from '../Command';
6
- import { BigNumberish } from 'ethers';
7
+ import { BigNumber, BigNumberish } from 'ethers';
7
8
  export declare type FlatFeeOptions = {
8
9
  amount: BigNumberish;
9
10
  recipient: string;
@@ -13,6 +14,8 @@ export declare type SwapOptions = Omit<RouterSwapOptions, 'inputTokenPermit'> &
13
14
  inputTokenPermit?: Permit2Permit;
14
15
  flatFee?: FlatFeeOptions;
15
16
  safeMode?: boolean;
17
+ maxHopSlippage?: BigNumber[];
18
+ urVersion?: URVersion;
16
19
  };
17
20
  export declare class UniswapTrade implements Command {
18
21
  trade: RouterTrade<Currency, Currency, TradeType>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { SwapRouter } from './swapRouter';
2
- export type { MigrateV3ToV4Options } from './swapRouter';
2
+ export type { MigrateV3ToV4Options, SignedRouteOptions, EIP712Payload } from './swapRouter';
3
3
  export * from './entities';
4
4
  export * from './utils/routerTradeAdapter';
5
5
  export { RoutePlanner, CommandType, COMMAND_DEFINITION, Parser, Subparser } from './utils/routerCommands';
@@ -8,3 +8,5 @@ export { UNIVERSAL_ROUTER_CREATION_BLOCK, UNIVERSAL_ROUTER_ADDRESS, ROUTER_AS_RE
8
8
  export { CommandParser, GenericCommandParser } from './utils/commandParser';
9
9
  export type { UniversalRouterCommand, UniversalRouterCall, Param, CommandsDefinition } from './utils/commandParser';
10
10
  export type { Permit2Permit } from './utils/inputTokens';
11
+ export { NONCE_SKIP_CHECK, generateNonce, EXECUTE_SIGNED_TYPES, getUniversalRouterDomain } from './utils/eip712';
12
+ export { URVersion } from '@uniswap/v4-sdk';
@@ -1,14 +1,35 @@
1
1
  import { Interface } from '@ethersproject/abi';
2
- import { BigNumberish } from 'ethers';
2
+ import { BigNumber, BigNumberish } from 'ethers';
3
3
  import { MethodParameters, Position as V3Position, RemoveLiquidityOptions as V3RemoveLiquidityOptions } from '@uniswap/v3-sdk';
4
4
  import { Position as V4Position, AddLiquidityOptions as V4AddLiquidityOptions } from '@uniswap/v4-sdk';
5
5
  import { Trade as RouterTrade } from '@uniswap/router-sdk';
6
6
  import { Currency, TradeType } from '@uniswap/sdk-core';
7
7
  import { SwapOptions } from './entities/actions/uniswap';
8
+ import { AcrossV4DepositV3Params } from './entities/actions/across';
9
+ import { TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer';
8
10
  export declare type SwapRouterConfig = {
9
11
  sender?: string;
10
12
  deadline?: BigNumberish;
11
13
  };
14
+ export declare type SignedRouteOptions = {
15
+ intent: string;
16
+ data: string;
17
+ sender: string;
18
+ nonce?: string;
19
+ };
20
+ export declare type EIP712Payload = {
21
+ domain: TypedDataDomain;
22
+ types: Record<string, TypedDataField[]>;
23
+ value: {
24
+ commands: string;
25
+ inputs: string[];
26
+ intent: string;
27
+ data: string;
28
+ sender: string;
29
+ nonce: string;
30
+ deadline: string;
31
+ };
32
+ };
12
33
  export interface MigrateV3ToV4Options {
13
34
  inputPosition: V3Position;
14
35
  outputPosition: V4Position;
@@ -17,7 +38,30 @@ export interface MigrateV3ToV4Options {
17
38
  }
18
39
  export declare abstract class SwapRouter {
19
40
  static INTERFACE: Interface;
20
- static swapCallParameters(trades: RouterTrade<Currency, Currency, TradeType>, options: SwapOptions): MethodParameters;
41
+ static swapCallParameters(trades: RouterTrade<Currency, Currency, TradeType>, options: SwapOptions, bridgeOptions?: AcrossV4DepositV3Params[]): MethodParameters;
42
+ /**
43
+ * Generate EIP712 payload for signed execution (no signing performed)
44
+ * Decodes existing execute() calldata and prepares it for signing
45
+ *
46
+ * @param calldata The calldata from swapCallParameters() or similar
47
+ * @param signedOptions Options for signed execution (intent, data, sender, nonce)
48
+ * @param deadline The deadline timestamp
49
+ * @param chainId The chain ID
50
+ * @param routerAddress The Universal Router contract address
51
+ * @returns EIP712 payload ready to be signed externally
52
+ */
53
+ static getExecuteSignedPayload(calldata: string, signedOptions: SignedRouteOptions, deadline: BigNumberish, chainId: number, routerAddress: string): EIP712Payload;
54
+ /**
55
+ * Encode executeSigned() call with signature
56
+ *
57
+ * @param calldata The original calldata from swapCallParameters()
58
+ * @param signature The signature obtained from external signing
59
+ * @param signedOptions The same options used in getExecuteSignedPayload()
60
+ * @param deadline The deadline timestamp
61
+ * @param nativeCurrencyValue The native currency value (ETH) to send
62
+ * @returns Method parameters for executeSigned()
63
+ */
64
+ static encodeExecuteSigned(calldata: string, signature: string, signedOptions: SignedRouteOptions, deadline: BigNumberish, nativeCurrencyValue?: BigNumber): MethodParameters;
21
65
  /**
22
66
  * Builds the call parameters for a migration from a V3 position to a V4 position.
23
67
  * Some requirements of the parameters: