Skip to content

Commit 7f0ab47

Browse files
committed
chore: navigate to smart exit list
1 parent de0f8c3 commit 7f0ab47

File tree

6 files changed

+198
-54
lines changed

6 files changed

+198
-54
lines changed
Lines changed: 8 additions & 0 deletions
Loading

apps/kyberswap-interface/src/components/Header/groups/EarnNavGroup.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ReactComponent as OverviewIcon } from 'assets/svg/earn/ic_earn_overview
77
import { ReactComponent as PoolsIcon } from 'assets/svg/earn/ic_earn_pools.svg'
88
import { ReactComponent as PositionsIcon } from 'assets/svg/earn/ic_earn_positions.svg'
99
import { ReactComponent as FarmingIcon } from 'assets/svg/earn/ic_farming.svg'
10+
import { ReactComponent as ListIcon } from 'assets/svg/ic_exit.svg'
1011
import { ReactComponent as KemIcon } from 'assets/svg/kyber/kem.svg'
1112
import NavGroup from 'components/Header/groups/NavGroup'
1213
import { DropdownTextAnchor, StyledNavLink } from 'components/Header/styleds'
@@ -25,6 +26,7 @@ const EarnNavGroup = () => {
2526
APP_PATHS.EARN_POOLS,
2627
APP_PATHS.EARN_POSITIONS,
2728
APP_PATHS.EARN_POSITION_DETAIL,
29+
APP_PATHS.EARN_SMART_EXIT,
2830
].some(path => pathname.includes(path))
2931

3032
return (
@@ -102,6 +104,12 @@ const EarnNavGroup = () => {
102104
{t`My Positions`}
103105
</Flex>
104106
</StyledNavLink>
107+
<StyledNavLink data-testid="earn-positions-nav-link" to={{ pathname: `${APP_PATHS.EARN_SMART_EXIT}` }}>
108+
<Flex sx={{ gap: '12px' }} alignItems="center">
109+
<ListIcon width={16} height={16} />
110+
{t`Smart Exit Orders`}
111+
</Flex>
112+
</StyledNavLink>
105113
</Flex>
106114
}
107115
/>

apps/kyberswap-interface/src/hooks/usePermitNft.ts

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface PermitNftParams {
2424
tokenId: string
2525
spender: string
2626
deadline?: number
27+
version?: 'v3' | 'v4' | 'auto' // specify version or auto-detect
2728
}
2829

2930
export interface PermitNftResult {
@@ -36,26 +37,49 @@ export interface PermitNftResult {
3637
// NFT Position Manager ABI for permit functionality
3738
const NFT_PERMIT_ABI = [
3839
'function name() view returns (string)',
39-
'function nonces(address owner, uint256 word) view returns (uint256 bitmap)',
40+
'function nonces(address owner, uint256 word) view returns (uint256 bitmap)', // V4 unordered nonces
41+
'function positions(uint256 tokenId) view returns (uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1)', // V3 ordered nonces
4042
'function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, bytes signature) payable',
4143
]
4244

4345
// 30 days validity buffer
4446
const PERMIT_NFT_VALIDITY_BUFFER = 30 * 24 * 60 * 60
4547

46-
export const usePermitNft = ({ contractAddress, tokenId, spender, deadline }: PermitNftParams) => {
48+
export const usePermitNft = ({ contractAddress, tokenId, spender, deadline, version = 'auto' }: PermitNftParams) => {
4749
const { account, chainId } = useActiveWeb3React()
4850
const { library } = useWeb3React()
4951
const notify = useNotify()
5052
const [isSigningInProgress, setIsSigningInProgress] = useState(false)
5153
const [permitData, setPermitData] = useState<PermitNftResult | null>(null)
54+
const [detectedVersion, setDetectedVersion] = useState<'v3' | 'v4' | null>(null)
5255

5356
const nftContract = useReadingContract(contractAddress, NFT_PERMIT_ABI)
5457

55-
// Get nonces bitmap for word 0
58+
// Get nonces bitmap for word 0 (V4 style)
5659
const noncesState = useSingleCallResult(nftContract, 'nonces', [account, 0])
60+
// Get position data (V3 style) - only call if we have a tokenId
61+
const positionsState = useSingleCallResult(nftContract, 'positions', tokenId ? [tokenId] : undefined)
5762
const nameState = useSingleCallResult(nftContract, 'name', [])
5863

64+
// Auto-detect version based on available data
65+
const actualVersion = useMemo(() => {
66+
if (version !== 'auto') return version
67+
68+
if (detectedVersion) return detectedVersion
69+
70+
// Try to detect based on available data
71+
if (positionsState?.result && !positionsState.error) {
72+
setDetectedVersion('v3')
73+
return 'v3'
74+
}
75+
if (noncesState?.result && !noncesState.error) {
76+
setDetectedVersion('v4')
77+
return 'v4'
78+
}
79+
80+
return 'v4' // Default to v4 if uncertain
81+
}, [version, detectedVersion, positionsState, noncesState])
82+
5983
const permitState = useMemo(() => {
6084
if (!account || !contractAddress || !tokenId || !spender) {
6185
return PermitNftState.NOT_APPLICABLE
@@ -79,12 +103,38 @@ export const usePermitNft = ({ contractAddress, tokenId, spender, deadline }: Pe
79103
throw new Error('No free nonce in word 0; pick a different word.')
80104
}, [])
81105

106+
// Get nonce based on version
107+
const getNonce = useCallback((): BigNumber | null => {
108+
if (actualVersion === 'v3') {
109+
// Use ordered nonce from positions function
110+
if (positionsState?.result?.[0] !== undefined) {
111+
return BigNumber.from(positionsState.result[0]).add(1) // Next nonce is current + 1
112+
}
113+
} else {
114+
// Use unordered nonce from bitmap (V4)
115+
if (noncesState?.result?.[0]) {
116+
return findFreeNonce(noncesState.result[0], 0)
117+
}
118+
}
119+
return null
120+
}, [actualVersion, positionsState?.result, noncesState?.result, findFreeNonce])
121+
82122
const signPermitNft = useCallback(async (): Promise<PermitNftResult | null> => {
83-
if (!library || !account || !chainId || !noncesState?.result?.[0] || !nameState?.result?.[0]) {
123+
if (!library || !account || !chainId || !nameState?.result?.[0]) {
84124
console.error('Missing required data for NFT permit')
85125
return null
86126
}
87127

128+
// Check version-specific requirements
129+
if (actualVersion === 'v3' && !positionsState?.result) {
130+
console.error('Missing positions data for V3 NFT permit')
131+
return null
132+
}
133+
if (actualVersion === 'v4' && !noncesState?.result?.[0]) {
134+
console.error('Missing nonces data for V4 NFT permit')
135+
return null
136+
}
137+
88138
if (permitState !== PermitNftState.READY_TO_SIGN) {
89139
console.error('NFT permit not ready to sign')
90140
return null
@@ -94,8 +144,12 @@ export const usePermitNft = ({ contractAddress, tokenId, spender, deadline }: Pe
94144

95145
try {
96146
const contractName = nameState.result[0]
97-
const bitmap = noncesState.result[0]
98-
const nonce = findFreeNonce(bitmap, 0)
147+
const nonce = getNonce()
148+
149+
if (!nonce) {
150+
throw new Error(`Failed to get nonce for ${actualVersion}`)
151+
}
152+
99153
const permitDeadline = deadline || Math.floor(Date.now() / 1000) + PERMIT_NFT_VALIDITY_BUFFER
100154

101155
// EIP-712 domain and types for NFT permit
@@ -135,17 +189,18 @@ export const usePermitNft = ({ contractAddress, tokenId, spender, deadline }: Pe
135189
message,
136190
})
137191

138-
console.log('Signing NFT permit with data:', typedData)
192+
console.log(`Signing ${actualVersion} NFT permit with data:`, typedData)
139193

140194
const signature = await library.send('eth_signTypedData_v4', [account.toLowerCase(), typedData])
141195

142196
// Encode permit data for contract call
143197
const permitData = defaultAbiCoder.encode(['uint256', 'uint256', 'bytes'], [permitDeadline, nonce, signature])
144198

199+
const v = actualVersion.toUpperCase()
145200
notify({
146201
type: NotificationType.SUCCESS,
147202
title: t`NFT Permit Signed`,
148-
summary: t`Successfully signed permit for NFT #${tokenId}`,
203+
summary: t`Successfully signed ${v} permit for NFT #${tokenId}`,
149204
})
150205

151206
const result = {
@@ -180,16 +235,32 @@ export const usePermitNft = ({ contractAddress, tokenId, spender, deadline }: Pe
180235
spender,
181236
deadline,
182237
permitState,
238+
actualVersion,
183239
noncesState?.result,
240+
positionsState?.result,
184241
nameState?.result,
185-
findFreeNonce,
242+
getNonce,
186243
notify,
187244
])
188245

246+
// Check readiness based on version
247+
const isReady = useMemo(() => {
248+
if (permitState !== PermitNftState.READY_TO_SIGN || !nameState?.result) {
249+
return false
250+
}
251+
252+
if (actualVersion === 'v3') {
253+
return !!positionsState?.result
254+
} else {
255+
return !!noncesState?.result
256+
}
257+
}, [permitState, nameState?.result, actualVersion, positionsState?.result, noncesState?.result])
258+
189259
return {
190260
permitState,
191261
signPermitNft,
192262
permitData,
193-
isReady: permitState === PermitNftState.READY_TO_SIGN && !!noncesState?.result && !!nameState?.result,
263+
isReady,
264+
version: actualVersion,
194265
}
195266
}

apps/kyberswap-interface/src/pages/Earns/SmartExit/index.tsx

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Trans } from '@lingui/macro'
2-
import { useEffect, useState } from 'react'
2+
import { Trash2 } from 'react-feather'
33
import { useNavigate } from 'react-router'
44
import { Flex, Text } from 'rebass'
55
import { useGetSmartExitOrdersQuery } from 'services/smartExit'
66
import { useUserPositionsQuery } from 'services/zapEarn'
77
import styled from 'styled-components'
88

9+
import LocalLoader from 'components/LocalLoader'
910
import TokenLogo from 'components/TokenLogo'
1011
import { useActiveWeb3React } from 'hooks'
1112
import useTheme from 'hooks/useTheme'
@@ -18,6 +19,17 @@ import useFilter from '../UserPositions/useFilter'
1819
import { earnSupportedChains, earnSupportedExchanges } from '../constants'
1920
import useSupportedDexesAndChains from '../hooks/useSupportedDexesAndChains'
2021

22+
const Trash = styled.div`
23+
width: 20px;
24+
height: 20px;
25+
cursor: pointer;
26+
color: ${({ theme }) => theme.subText};
27+
28+
:hover {
29+
color: ${({ theme }) => theme.red};
30+
}
31+
`
32+
2133
const TableHeader = styled.div`
2234
display: grid;
2335
grid-template-columns: 1fr 1fr 0.5fr 40px;
@@ -35,44 +47,37 @@ const TableRow = styled(TableHeader)`
3547
const SmartExit = () => {
3648
const theme = useTheme()
3749
const navigate = useNavigate()
38-
const { account, chainId } = useActiveWeb3React()
50+
const { account } = useActiveWeb3React()
3951

40-
const [loading, setLoading] = useState(false)
4152
const { filters, updateFilters } = useFilter()
4253
const { supportedDexes, supportedChains } = useSupportedDexesAndChains(filters)
4354

4455
// Fetch smart exit orders
4556
const {
4657
data: orders = [],
47-
isLoading: ordersLoading,
58+
isLoading: smartExitLoading,
4859
error: ordersError,
4960
} = useGetSmartExitOrdersQuery(
5061
{
51-
chainId: chainId,
62+
// chainId: chainId,
5263
userWallet: account || '',
5364
},
5465
{
55-
skip: !account || !chainId,
66+
skip: !account,
5667
pollingInterval: 30000, // Poll every 30 seconds
5768
},
5869
)
5970

60-
useEffect(() => {
61-
if (ordersLoading) {
62-
setLoading(true)
63-
} else {
64-
setLoading(false)
65-
}
66-
}, [ordersLoading])
67-
68-
const { data: userPosition } = useUserPositionsQuery(
71+
const { data: userPosition, isLoading: userPosLoading } = useUserPositionsQuery(
6972
{ chainIds: earnSupportedChains.join(','), addresses: account || '', protocols: earnSupportedExchanges.join(',') },
7073
{
7174
skip: !account,
7275
pollingInterval: 15_000,
7376
},
7477
)
7578

79+
const loading = smartExitLoading || userPosLoading
80+
7681
return (
7782
<PoolPageWrapper>
7883
<Flex alignItems="center" sx={{ gap: 3 }}>
@@ -88,7 +93,6 @@ const SmartExit = () => {
8893
filters={filters}
8994
updateFilters={(...args) => {
9095
updateFilters(...args)
91-
setLoading(true)
9296
}}
9397
/>
9498

@@ -108,9 +112,7 @@ const SmartExit = () => {
108112

109113
{loading ? (
110114
<Flex justifyContent="center" padding="20px">
111-
<Text color="subText">
112-
<Trans>Loading orders...</Trans>
113-
</Text>
115+
<LocalLoader />
114116
</Flex>
115117
) : ordersError ? (
116118
<Flex justifyContent="center" padding="20px">
@@ -126,14 +128,27 @@ const SmartExit = () => {
126128
</Flex>
127129
) : (
128130
orders.map(order => {
129-
const posDetail = userPosition?.find(
130-
us => us.tokenId === order.positionId && +us.chainId === +order.chainId,
131-
)
131+
const posDetail = userPosition?.find(us => order.positionId === us.id)
132132
if (!posDetail) return null
133133
const token0 = posDetail.currentAmounts[0].token
134134
const token1 = posDetail.currentAmounts[1].token
135+
const tokenId = order.positionId.split('-')[1]
135136

136137
const { conditions, op } = order.condition.logical
138+
139+
const protocol = (() => {
140+
switch (posDetail.pool.project) {
141+
case 'Uniswap V3':
142+
return
143+
case 'Uniswap V4':
144+
return 'V4'
145+
case 'Uniswap V4 FairFlow':
146+
return 'FairFlow'
147+
default:
148+
return posDetail.pool.project
149+
}
150+
})()
151+
137152
return (
138153
<TableRow key={order.id}>
139154
<div>
@@ -151,7 +166,7 @@ const SmartExit = () => {
151166
<Flex alignItems="center" sx={{ gap: '4px' }} mt="4px" ml="1rem">
152167
<TokenLogo src={posDetail.pool.projectLogo} size={16} />
153168
<Text color={theme.subText}>
154-
{posDetail.pool.project} #{order.positionId}
169+
{protocol} #{tokenId}
155170
</Text>
156171
</Flex>
157172
</div>
@@ -221,7 +236,9 @@ const SmartExit = () => {
221236
: order.status.charAt(0).toUpperCase() + order.status.slice(1)}
222237
</Badge>
223238
</Flex>
224-
<div></div>
239+
<Trash>
240+
<Trash2 size={18} />
241+
</Trash>
225242
</TableRow>
226243
)
227244
})

0 commit comments

Comments
 (0)