Skip to content

Commit b942226

Browse files
authored
fix: Cap max expiry for setTimeout in useIsExpiredSwap (#4674)
* fix: Cap max expiry for setTimeout in useIsExpiredSwap * refactor: Extract logic and remove Math.abs
1 parent 7e88d74 commit b942226

File tree

2 files changed

+128
-12
lines changed

2 files changed

+128
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { act } from 'react'
2+
import useIsExpiredSwap from '@/features/swap/hooks/useIsExpiredSwap'
3+
import { renderHook } from '@/tests/test-utils'
4+
import * as guards from '@/utils/transaction-guards'
5+
import type { TransactionInfo } from '@safe-global/safe-gateway-typescript-sdk'
6+
7+
describe('useIsExpiredSwap', () => {
8+
beforeEach(() => {
9+
jest.useFakeTimers()
10+
jest.clearAllMocks()
11+
})
12+
13+
afterEach(() => {
14+
jest.useRealTimers()
15+
})
16+
17+
it('returns false if txInfo is not a swap order', () => {
18+
jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(false)
19+
20+
const txInfo = {} as TransactionInfo
21+
const { result } = renderHook(() => useIsExpiredSwap(txInfo))
22+
23+
expect(result.current).toBe(false)
24+
})
25+
26+
it('returns true if the swap has already expired', () => {
27+
// Mock so that txInfo is considered a swap order
28+
jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(true)
29+
30+
const now = Date.now()
31+
const pastUnixTime = Math.floor((now - 1000) / 1000) // 1 second in the past
32+
const txInfo = { validUntil: pastUnixTime } as TransactionInfo
33+
34+
const { result } = renderHook(() => useIsExpiredSwap(txInfo))
35+
36+
// Since expiry is in the past, should return true immediately
37+
expect(result.current).toBe(true)
38+
})
39+
40+
it('returns false initially and true after expiry time if the swap has not yet expired', () => {
41+
jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(true)
42+
43+
const now = Date.now()
44+
// set expiry 2 seconds in the future
45+
const futureUnixTime = Math.floor((now + 2000) / 1000)
46+
const txInfo = { validUntil: futureUnixTime } as TransactionInfo
47+
48+
const { result, unmount } = renderHook(() => useIsExpiredSwap(txInfo))
49+
50+
// Initially should be false because it hasn't expired yet
51+
expect(result.current).toBe(false)
52+
53+
// Advance time by 2 seconds to simulate waiting until expiry
54+
act(() => {
55+
jest.advanceTimersByTime(2000)
56+
})
57+
58+
// After the timer completes, it should become true
59+
expect(result.current).toBe(true)
60+
61+
// Unmount to ensure cleanup runs without errors
62+
unmount()
63+
})
64+
65+
it('cancels the timeout when unmounted', () => {
66+
jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(true)
67+
68+
const now = Date.now()
69+
// set expiry 5 seconds in the future
70+
const futureUnixTime = Math.floor((now + 5000) / 1000)
71+
const txInfo = { validUntil: futureUnixTime } as TransactionInfo
72+
73+
const { result, unmount } = renderHook(() => useIsExpiredSwap(txInfo))
74+
expect(result.current).toBe(false)
75+
76+
// Unmount the hook before the timer finishes
77+
unmount()
78+
79+
// Advance time to ensure no setState runs after unmount
80+
act(() => {
81+
jest.advanceTimersByTime(5000)
82+
})
83+
})
84+
85+
it('uses MAX_TIMEOUT if the validUntil value is too large', () => {
86+
jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(true)
87+
88+
const MAX_TIMEOUT = 2147483647
89+
90+
const now = Date.now()
91+
// Set validUntil so far in the future that timeUntilExpiry would exceed MAX_TIMEOUT
92+
const largeFutureTime = Math.floor((now + MAX_TIMEOUT + 10_000) / 1000)
93+
const txInfo = { validUntil: largeFutureTime } as TransactionInfo
94+
95+
const { result } = renderHook(() => useIsExpiredSwap(txInfo))
96+
expect(result.current).toBe(false)
97+
98+
// The timeout should be capped at MAX_TIMEOUT
99+
// Advance time by MAX_TIMEOUT and check if expired is now true
100+
act(() => {
101+
jest.advanceTimersByTime(MAX_TIMEOUT)
102+
})
103+
104+
expect(result.current).toBe(true)
105+
})
106+
})

src/features/swap/hooks/useIsExpiredSwap.ts

+22-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ import { useEffect, useRef, useState } from 'react'
22
import type { TransactionInfo } from '@safe-global/safe-gateway-typescript-sdk'
33
import { isSwapOrderTxInfo } from '@/utils/transaction-guards'
44

5+
// https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value
6+
const MAX_TIMEOUT = 2147483647
7+
8+
function getExpiryDelay(expiryUnixTimestampSec: number): number {
9+
const currentTimeMs = Date.now()
10+
const expiryTimeMs = expiryUnixTimestampSec * 1000
11+
const timeUntilExpiry = expiryTimeMs - currentTimeMs
12+
13+
if (timeUntilExpiry <= 0) {
14+
return 0 // Already expired
15+
}
16+
17+
return Math.min(timeUntilExpiry, MAX_TIMEOUT)
18+
}
19+
520
/**
621
* Checks whether a swap has expired and if it hasn't it sets a timeout
722
* for the exact moment it will expire
@@ -14,22 +29,17 @@ const useIsExpiredSwap = (txInfo: TransactionInfo) => {
1429
useEffect(() => {
1530
if (!isSwapOrderTxInfo(txInfo)) return
1631

17-
const checkExpiry = () => {
18-
const now = Date.now()
19-
const expiryTime = txInfo.validUntil * 1000
32+
const delay = getExpiryDelay(txInfo.validUntil)
2033

21-
if (now > expiryTime) {
34+
if (delay === 0) {
35+
setIsExpired(true)
36+
} else {
37+
// Set a timeout for the exact moment it will expire
38+
timerRef.current = setTimeout(() => {
2239
setIsExpired(true)
23-
} else {
24-
// Set a timeout for the exact moment it will expire
25-
timerRef.current = setTimeout(() => {
26-
setIsExpired(true)
27-
}, expiryTime - now)
28-
}
40+
}, delay)
2941
}
3042

31-
checkExpiry()
32-
3343
return () => {
3444
if (timerRef.current) {
3545
clearTimeout(timerRef.current)

0 commit comments

Comments
 (0)