Skip to content

Commit fb39602

Browse files
ci(release): publish latest release
1 parent 375abbe commit fb39602

13 files changed

Lines changed: 520 additions & 15 deletions

File tree

RELEASE

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
IPFS hash of the deployment:
2-
- CIDv0: `QmNxYFPwWseSvpgkBN5c3vDewpbsEdSuXgJF7epxQZezCU`
3-
- CIDv1: `bafybeiajggnkyo4mxc4va5sl4bj6vmex7jm7pd557roqdyc3rqiq2bebau`
2+
- CIDv0: `QmPjuGM1wsScvkB1LSBa22Fm61tgEQ8qRVoimnXCxkuNfG`
3+
- CIDv1: `bafybeiau2cap5oub3nn6jg6u4qdl7ftf3az7bof3b5mu26xiy2l326q4r4`
44

55
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
66

@@ -10,5 +10,5 @@ You can also access the Uniswap Interface from an IPFS gateway.
1010
Your Uniswap settings are never remembered across different URLs.
1111

1212
IPFS gateways:
13-
- https://bafybeiajggnkyo4mxc4va5sl4bj6vmex7jm7pd557roqdyc3rqiq2bebau.ipfs.dweb.link/
14-
- [ipfs://QmNxYFPwWseSvpgkBN5c3vDewpbsEdSuXgJF7epxQZezCU/](ipfs://QmNxYFPwWseSvpgkBN5c3vDewpbsEdSuXgJF7epxQZezCU/)
13+
- https://bafybeiau2cap5oub3nn6jg6u4qdl7ftf3az7bof3b5mu26xiy2l326q4r4.ipfs.dweb.link/
14+
- [ipfs://QmPjuGM1wsScvkB1LSBa22Fm61tgEQ8qRVoimnXCxkuNfG/](ipfs://QmPjuGM1wsScvkB1LSBa22Fm61tgEQ8qRVoimnXCxkuNfG/)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web/5.151.1
1+
web/5.151.2

apps/web/src/components/FeatureFlagModal/flagGroups.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export function buildFlagGroups(extras: {
5353
{ flag: FeatureFlags.GasFeeOverrides, label: 'Enable Custom Gas Fee Overrides' },
5454
{ flag: FeatureFlags.UniquoteEnabled, label: 'Enable Uniquote' },
5555
{ flag: FeatureFlags.UnirouteEnabled, label: 'Enable Uniroute' },
56+
{ flag: FeatureFlags.RequestSwapSteps, label: 'Request SwapSteps in classic quotes' },
5657
{ flag: FeatureFlags.UseUniversalRouterVersion211, label: 'Use Universal Router v2.1.1' },
5758
{ flag: FeatureFlags.ViemProviderEnabled, label: 'Enable Viem Provider' },
5859
{ flag: FeatureFlags.LimitsFees, label: 'Enable Limits fees' },

packages/api/src/clients/trading/api.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6474,6 +6474,15 @@
64746474
],
64756475
"description": "The protocol of the pool."
64766476
},
6477+
"ProtocolItemsAmm": {
6478+
"type": "string",
6479+
"enum": [
6480+
"V2",
6481+
"V3",
6482+
"V4"
6483+
],
6484+
"description": "The protocol of the pool."
6485+
},
64776486
"encodedOrder": {
64786487
"type": "string",
64796488
"description": "An encoded copy of the order details which will be submitted to the filler network along with the signed permit."

packages/gating/src/flags.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export enum FeatureFlags {
4343
RwaUxSearch,
4444
RwaUxSearchTop24hSection,
4545
RandomizeQuotePolling,
46+
RequestSwapSteps,
4647
Robinhood,
4748
SessionsPerformanceTrackingEnabled,
4849
SessionsServiceEnabled,
@@ -148,6 +149,7 @@ export const SHARED_FEATURE_FLAG_NAMES = new Map<FeatureFlags, string>([
148149
[FeatureFlags.RWAUXExplore, 'rwa_ux_explore'],
149150
[FeatureFlags.RWAUXExploreCarousel, 'rwa_ux_explore_carousel'],
150151
[FeatureFlags.RandomizeQuotePolling, 'randomize_quote_polling'],
152+
[FeatureFlags.RequestSwapSteps, 'request_swap_steps'],
151153
[FeatureFlags.Robinhood, 'robinhood'],
152154
[FeatureFlags.RwaGeoblocked, 'rwa_geo_blocked'],
153155
[FeatureFlags.RwaUxSearch, 'rwa_ux_search'],

packages/uniswap/src/data/apiClients/tradingApi/TradingApiClient.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,19 @@ describe('getFeatureFlaggedHeaders', () => {
573573
break
574574
}
575575
})
576+
577+
it(`Endpoint: ${path} should/should not include RequestSwapSteps header when feature flag is enabled`, async () => {
578+
mockGetFeatureFlag.mockImplementation((flag) => flag === FeatureFlags.RequestSwapSteps)
579+
const headers = await getFeatureFlaggedHeaders(path)
580+
switch (path) {
581+
case TRADING_API_PATHS.quote:
582+
expect(headers).toHaveProperty(TradingApiHeaders.RequestSwapSteps, 'true')
583+
break
584+
default:
585+
expect(headers).not.toHaveProperty(TradingApiHeaders.RequestSwapSteps)
586+
break
587+
}
588+
})
576589
})
577590
})
578591

packages/uniswap/src/data/apiClients/tradingApi/TradingApiClient.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ function addHeaderIfEnabled(params: { headers: Record<string, string>; key: stri
5555

5656
export enum TradingApiHeaders {
5757
UniversalRouterVersion = 'x-universal-router-version',
58+
RequestSwapSteps = 'x-universal-router-swapsteps',
5859
UniquoteEnabled = 'x-uniquote-enabled',
5960
ViemProviderEnabled = 'x-viem-provider-enabled',
6061
Erc20EthEnabled = 'x-erc20eth-enabled',
@@ -91,6 +92,7 @@ export const getFeatureFlaggedHeaders = async (
9192

9293
const chainedActionsEnabled = getFeatureFlag(FeatureFlags.ChainedActions)
9394
const unirouteEnabled = getFeatureFlag(FeatureFlags.UnirouteEnabled)
95+
const shouldRequestSwapSteps = getFeatureFlag(FeatureFlags.RequestSwapSteps)
9496
const ethAsErc20UniswapXEnabled = getExperimentValueFromLayer<
9597
typeof Layers.SwapPage,
9698
Experiments.EthAsErc20UniswapX,
@@ -108,6 +110,7 @@ export const getFeatureFlaggedHeaders = async (
108110
headers[TradingApiHeaders.UniroutePulumiEnabled] = 'true'
109111
addHeaderIfEnabled({ headers, key: TradingApiHeaders.Erc20EthEnabled, enabled: ethAsErc20UniswapXEnabled })
110112
addHeaderIfEnabled({ headers, key: TradingApiHeaders.ChainedActionsEnabled, enabled: chainedActionsEnabled })
113+
addHeaderIfEnabled({ headers, key: TradingApiHeaders.RequestSwapSteps, enabled: shouldRequestSwapSteps })
111114
addHeaderIfEnabled({
112115
headers,
113116
key: TradingApiHeaders.DisableUniswapInterfaceFees,

packages/uniswap/src/features/transactions/swap/components/RoutingInfo/RoutingHopInfo.tsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
BestRouteUniswapXTooltip,
2222
} from 'uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormTooltips/BestRouteTooltip'
2323
import { Trade } from 'uniswap/src/features/transactions/swap/types/trade'
24-
import { isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing'
24+
import { isClassic, isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing'
25+
import { summarizeSwapSteps } from 'uniswap/src/utils/routingDiagram/routingProviders/uniswapRoutingProvider'
2526
import { useRoutingEntries, useRoutingProvider } from 'uniswap/src/utils/routingDiagram/routingRegistry'
2627
import { NumberType } from 'utilities/src/format/types'
2728

@@ -44,6 +45,18 @@ export function RoutingHopInfo({
4445
const routingProvider = useRoutingProvider({ routing: trade.routing })
4546
const isUniswapXTrade = isUniswapX(trade)
4647

48+
const swapStepsSummary = useMemo(() => {
49+
const steps = isClassic(trade) ? trade.quote.quote.swapSteps : undefined
50+
if (!steps?.length) {
51+
return undefined
52+
}
53+
const { pools, versions } = summarizeSwapSteps(steps)
54+
if (pools <= 0 || versions.length === 0) {
55+
return undefined
56+
}
57+
return { pools, versions }
58+
}, [trade])
59+
4760
const caption = useMemo(() => {
4861
const textVariant = isWebPlatform ? 'body4' : 'body2'
4962
const textAlign = isWebPlatform ? 'left' : 'center'
@@ -65,6 +78,17 @@ export function RoutingHopInfo({
6578
)
6679
}
6780

81+
if (swapStepsSummary) {
82+
return (
83+
<Text variant={textVariant} textAlign={textAlign} color="$neutral2">
84+
{t('swap.routing.poolsAndVersions', {
85+
count: swapStepsSummary.pools,
86+
versions: swapStepsSummary.versions.join(', '),
87+
})}
88+
</Text>
89+
)
90+
}
91+
6892
if (routes) {
6993
const descriptionText =
7094
routingProvider?.getDescription?.(t) ??
@@ -86,7 +110,7 @@ export function RoutingHopInfo({
86110
)
87111
}
88112
return null
89-
}, [t, trade, routes, gasFeeFormatted, routingProvider, isUniswapXTrade])
113+
}, [t, trade, routes, gasFeeFormatted, routingProvider, isUniswapXTrade, swapStepsSummary])
90114

91115
const modalTitle =
92116
!isUniswapXTrade && routingProvider?.name
@@ -96,15 +120,15 @@ export function RoutingHopInfo({
96120
const ModalIcon = routingProvider?.icon ?? OrderRouting
97121
const modalIconColor = routingProvider?.iconColor ?? '$neutral1'
98122

99-
const mobileLearnMore =
100-
isMobileApp && !isUniswapXTrade ? (
101-
<LearnMoreLink textVariant="buttonLabel3" url={UniswapHelpUrls.articles.routingSettings} />
123+
const learnMoreLink =
124+
!isUniswapXTrade && (isMobileApp || swapStepsSummary || !routes || routes.length === 0) ? (
125+
<LearnMoreLink textVariant="buttonLabel4" textColor="$neutral1" url={UniswapHelpUrls.articles.routingSettings} />
102126
) : undefined
103127

104128
return (
105129
<Flex row alignItems="center" justifyContent="space-between">
106130
<WarningInfo
107-
infoButton={mobileLearnMore}
131+
infoButton={learnMoreLink}
108132
modalProps={{
109133
modalName: ModalName.SwapReview,
110134
captionComponent: caption,
@@ -118,6 +142,8 @@ export function RoutingHopInfo({
118142
tooltipProps={{
119143
text: isUniswapXTrade ? (
120144
<BestRouteUniswapXTooltip />
145+
) : swapStepsSummary ? (
146+
caption
121147
) : routes && routes.length > 0 ? (
122148
<BestRouteTooltip />
123149
) : (

packages/uniswap/src/features/transactions/swap/utils/trade.test.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Protocol } from '@uniswap/router-sdk'
12
import { TradeType } from '@uniswap/sdk-core'
23
import { type ClassicQuoteResponse, TradingApi } from '@universe/api'
34
import { UNI, WBTC } from 'uniswap/src/constants/tokens'
@@ -8,7 +9,7 @@ import {
89
type BridgeTrade,
910
type ClassicTrade,
1011
} from 'uniswap/src/features/transactions/swap/types/trade'
11-
import { requireAcceptNewTrade } from 'uniswap/src/features/transactions/swap/utils/trade'
12+
import { getProtocolVersionFromTrade, requireAcceptNewTrade } from 'uniswap/src/features/transactions/swap/utils/trade'
1213

1314
const INPUT_TOKEN = UNI[UniverseChainId.Mainnet]
1415

@@ -142,3 +143,113 @@ describe(requireAcceptNewTrade, () => {
142143
})
143144
})
144145
})
146+
147+
const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000'
148+
149+
function v2SwapStep(): TradingApi.SwapStep {
150+
return {
151+
type: 'V2_SWAP_EXACT_IN' as TradingApi.V2SwapExactInStep['type'],
152+
recipient: ADDRESS_ZERO,
153+
amountIn: '0',
154+
amountOutMin: '0',
155+
path: [ADDRESS_ZERO, ADDRESS_ZERO],
156+
} as TradingApi.SwapStep
157+
}
158+
159+
function v3SwapStep(): TradingApi.SwapStep {
160+
return {
161+
type: 'V3_SWAP_EXACT_IN' as TradingApi.V3SwapExactInStep['type'],
162+
recipient: ADDRESS_ZERO,
163+
amountIn: '0',
164+
amountOutMin: '0',
165+
path: '0x',
166+
} as TradingApi.SwapStep
167+
}
168+
169+
function v4SwapStep(): TradingApi.SwapStep {
170+
return {
171+
type: 'V4_SWAP' as TradingApi.V4SwapStep['type'],
172+
v4Actions: [
173+
{
174+
action: 'SWAP_EXACT_IN_SINGLE' as TradingApi.V4SwapExactInSingle['action'],
175+
poolKey: {} as TradingApi.SwapPoolKey,
176+
zeroForOne: true,
177+
amountIn: '0',
178+
amountOutMinimum: '0',
179+
hookData: '0x',
180+
} as TradingApi.V4Action,
181+
],
182+
} as TradingApi.SwapStep
183+
}
184+
185+
const wrapEthStep = {
186+
type: 'WRAP_ETH' as TradingApi.WrapEthStep['type'],
187+
recipient: ADDRESS_ZERO,
188+
amount: '0',
189+
} as TradingApi.SwapStep
190+
191+
// Builds a classic trade carrying the given `swapSteps`, with a single V3 quote-route hop so the
192+
// route-based fallback resolves to V3 whenever `swapSteps` are absent or contribute no version.
193+
const createClassicTradeWithSwapSteps = (swapSteps?: TradingApi.SwapStep[]): ClassicTrade => {
194+
const trade = createClassicTradeFromQuote({
195+
quote: {
196+
requestId: '123',
197+
routing: TradingApi.Routing.CLASSIC,
198+
permitData: null,
199+
quote: {
200+
input: { amount: '100', maximumAmount: '100', token: INPUT_TOKEN.address },
201+
output: { amount: '100', minimumAmount: '100', token: WBTC.address, recipient: '0xrecipient' },
202+
swapper: '0xswapper',
203+
route: [[{ type: 'v3-pool' }]],
204+
swapSteps,
205+
},
206+
} as ClassicQuoteResponse,
207+
currencyIn: INPUT_TOKEN,
208+
currencyOut: WBTC,
209+
tradeType: TradeType.EXACT_INPUT,
210+
deadline: Date.now() + 60 * 30 * 1000,
211+
})
212+
213+
if (!trade) {
214+
throw new Error('Expected test classic trade to be created')
215+
}
216+
217+
return trade
218+
}
219+
220+
describe(getProtocolVersionFromTrade, () => {
221+
it('returns undefined for non-classic trades', () => {
222+
expect(getProtocolVersionFromTrade(createBridgeTestTrade('1000', '990'))).toBeUndefined()
223+
})
224+
225+
describe('with swapSteps (new routing field)', () => {
226+
it('returns V2 when swapSteps contain only V2 swaps', () => {
227+
expect(getProtocolVersionFromTrade(createClassicTradeWithSwapSteps([v2SwapStep()]))).toBe(Protocol.V2)
228+
})
229+
230+
it('returns V3 when swapSteps contain only V3 swaps', () => {
231+
expect(getProtocolVersionFromTrade(createClassicTradeWithSwapSteps([v3SwapStep()]))).toBe(Protocol.V3)
232+
})
233+
234+
it('returns V4 when swapSteps contain only V4 swaps', () => {
235+
expect(getProtocolVersionFromTrade(createClassicTradeWithSwapSteps([v4SwapStep()]))).toBe(Protocol.V4)
236+
})
237+
238+
it('returns MIXED when swapSteps span multiple protocol versions', () => {
239+
expect(getProtocolVersionFromTrade(createClassicTradeWithSwapSteps([v2SwapStep(), v3SwapStep()]))).toBe(
240+
Protocol.MIXED,
241+
)
242+
})
243+
244+
it('falls back to route inference when swapSteps contribute no versions (wrap/unwrap only)', () => {
245+
// wrap/unwrap steps yield no protocol versions, so the V3 SDK route resolves the protocol.
246+
expect(getProtocolVersionFromTrade(createClassicTradeWithSwapSteps([wrapEthStep]))).toBe(Protocol.V3)
247+
})
248+
})
249+
250+
describe('without swapSteps (route fallback)', () => {
251+
it('infers the protocol from the SDK routes', () => {
252+
expect(getProtocolVersionFromTrade(createClassicTradeWithSwapSteps(undefined))).toBe(Protocol.V3)
253+
})
254+
})
255+
})

packages/uniswap/src/features/transactions/swap/utils/trade.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,15 +209,76 @@ export function getRateToDisplay({
209209
return showInverseRate ? rate : inverseRate
210210
}
211211

212+
/**
213+
* Derives the protocol version-set from a quote's `swapSteps` and maps it to a single `Protocol`.
214+
* A single version maps to its matching protocol; multiple versions are reported as MIXED. Returns
215+
* undefined when no swap versions are present (e.g. pure wrap/unwrap steps), letting the caller fall
216+
* back to route-based inference.
217+
*
218+
* Reads `swapSteps` directly rather than reusing `summarizeSwapSteps` (which also computes pool
219+
* counts and lives alongside the UI/routing-diagram layer) to keep this data util free of that
220+
* dependency — importing it introduces a circular dependency through `swap/utils/routing`.
221+
*/
222+
function getProtocolFromSwapSteps(steps: readonly TradingApi.SwapStep[]): Protocol | undefined {
223+
const versions = new Set<Protocol.V2 | Protocol.V3 | Protocol.V4>()
224+
225+
for (const step of steps) {
226+
switch (step.type) {
227+
case 'V2_SWAP_EXACT_IN':
228+
case 'V2_SWAP_EXACT_OUT':
229+
versions.add(Protocol.V2)
230+
break
231+
case 'V3_SWAP_EXACT_IN':
232+
case 'V3_SWAP_EXACT_OUT':
233+
versions.add(Protocol.V3)
234+
break
235+
case 'V4_SWAP':
236+
for (const action of step.v4Actions) {
237+
switch (action.action) {
238+
case 'SWAP_EXACT_IN':
239+
case 'SWAP_EXACT_OUT':
240+
case 'SWAP_EXACT_IN_SINGLE':
241+
case 'SWAP_EXACT_OUT_SINGLE':
242+
versions.add(Protocol.V4)
243+
break
244+
// SETTLE / TAKE action variants contribute no protocol version.
245+
}
246+
}
247+
break
248+
// WRAP_ETH / UNWRAP_WETH steps contribute no protocol version.
249+
}
250+
}
251+
252+
if (versions.size === 0) {
253+
return undefined
254+
}
255+
if (versions.size > 1) {
256+
return Protocol.MIXED
257+
}
258+
const [version] = versions
259+
return version
260+
}
261+
212262
export function getProtocolVersionFromTrade(trade: Trade): Protocol | undefined {
213263
if (!isClassic(trade)) {
214264
return undefined
215265
}
216266

267+
// Prefer the new `swapSteps` routing field when present: it is the canonical Universal Router
268+
// step sequence and correctly distinguishes V4 (which the route-based fallback below cannot).
269+
// Only populated when the GuideStar quoter wins the hybrid quote race.
270+
const { swapSteps } = trade.quote.quote
271+
if (swapSteps?.length) {
272+
const protocol = getProtocolFromSwapSteps(swapSteps)
273+
if (protocol) {
274+
return protocol
275+
}
276+
}
277+
278+
// Fallback for quotes without `swapSteps`: infer from the classic quote route hop types.
279+
// Note: route hops cannot express V4, so V4 is only detectable via `swapSteps` above.
217280
const route = trade.quote.quote.route ?? []
218281
const protocols = new Set(route.flatMap((hops) => hops.map((hop) => hop.type)))
219-
220-
// TODO(SWAP-2724): Update to properly extract from newer routing response types
221282
if (protocols.size === 0) {
222283
return undefined
223284
}

0 commit comments

Comments
 (0)