+
{formatTimeInWords(validUntil * 1000)}
{' '}
({formatDateTime(validUntil * 1000)})
@@ -124,6 +120,7 @@ export const SwapOrderConfirmation = ({ order, decodedData, settlementContract }
),
]}
/>
+
{isTwapOrder && (
(
)
const OnlyToken = ({ token }: { token: OrderToken }) => (
-
+
{token.symbol}
@@ -43,22 +35,15 @@ export const SwapTx = ({ info }: { info: Order }): ReactElement => {
return (
{from}
-
+
to
{to}
diff --git a/src/features/swap/hooks/__tests__/useIsExpiredSwap.test.ts b/src/features/swap/hooks/__tests__/useIsExpiredSwap.test.ts
new file mode 100644
index 0000000000..b62a8130fc
--- /dev/null
+++ b/src/features/swap/hooks/__tests__/useIsExpiredSwap.test.ts
@@ -0,0 +1,106 @@
+import { act } from 'react'
+import useIsExpiredSwap from '@/features/swap/hooks/useIsExpiredSwap'
+import { renderHook } from '@/tests/test-utils'
+import * as guards from '@/utils/transaction-guards'
+import type { TransactionInfo } from '@safe-global/safe-gateway-typescript-sdk'
+
+describe('useIsExpiredSwap', () => {
+ beforeEach(() => {
+ jest.useFakeTimers()
+ jest.clearAllMocks()
+ })
+
+ afterEach(() => {
+ jest.useRealTimers()
+ })
+
+ it('returns false if txInfo is not a swap order', () => {
+ jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(false)
+
+ const txInfo = {} as TransactionInfo
+ const { result } = renderHook(() => useIsExpiredSwap(txInfo))
+
+ expect(result.current).toBe(false)
+ })
+
+ it('returns true if the swap has already expired', () => {
+ // Mock so that txInfo is considered a swap order
+ jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(true)
+
+ const now = Date.now()
+ const pastUnixTime = Math.floor((now - 1000) / 1000) // 1 second in the past
+ const txInfo = { validUntil: pastUnixTime } as TransactionInfo
+
+ const { result } = renderHook(() => useIsExpiredSwap(txInfo))
+
+ // Since expiry is in the past, should return true immediately
+ expect(result.current).toBe(true)
+ })
+
+ it('returns false initially and true after expiry time if the swap has not yet expired', () => {
+ jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(true)
+
+ const now = Date.now()
+ // set expiry 2 seconds in the future
+ const futureUnixTime = Math.floor((now + 2000) / 1000)
+ const txInfo = { validUntil: futureUnixTime } as TransactionInfo
+
+ const { result, unmount } = renderHook(() => useIsExpiredSwap(txInfo))
+
+ // Initially should be false because it hasn't expired yet
+ expect(result.current).toBe(false)
+
+ // Advance time by 2 seconds to simulate waiting until expiry
+ act(() => {
+ jest.advanceTimersByTime(2000)
+ })
+
+ // After the timer completes, it should become true
+ expect(result.current).toBe(true)
+
+ // Unmount to ensure cleanup runs without errors
+ unmount()
+ })
+
+ it('cancels the timeout when unmounted', () => {
+ jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(true)
+
+ const now = Date.now()
+ // set expiry 5 seconds in the future
+ const futureUnixTime = Math.floor((now + 5000) / 1000)
+ const txInfo = { validUntil: futureUnixTime } as TransactionInfo
+
+ const { result, unmount } = renderHook(() => useIsExpiredSwap(txInfo))
+ expect(result.current).toBe(false)
+
+ // Unmount the hook before the timer finishes
+ unmount()
+
+ // Advance time to ensure no setState runs after unmount
+ act(() => {
+ jest.advanceTimersByTime(5000)
+ })
+ })
+
+ it('uses MAX_TIMEOUT if the validUntil value is too large', () => {
+ jest.spyOn(guards, 'isSwapOrderTxInfo').mockReturnValue(true)
+
+ const MAX_TIMEOUT = 2147483647
+
+ const now = Date.now()
+ // Set validUntil so far in the future that timeUntilExpiry would exceed MAX_TIMEOUT
+ const largeFutureTime = Math.floor((now + MAX_TIMEOUT + 10_000) / 1000)
+ const txInfo = { validUntil: largeFutureTime } as TransactionInfo
+
+ const { result } = renderHook(() => useIsExpiredSwap(txInfo))
+ expect(result.current).toBe(false)
+
+ // The timeout should be capped at MAX_TIMEOUT
+ // Advance time by MAX_TIMEOUT and check if expired is now true
+ act(() => {
+ jest.advanceTimersByTime(MAX_TIMEOUT)
+ })
+
+ expect(result.current).toBe(true)
+ })
+})
diff --git a/src/features/swap/hooks/useIsExpiredSwap.ts b/src/features/swap/hooks/useIsExpiredSwap.ts
index 82915d7e7a..efe89de598 100644
--- a/src/features/swap/hooks/useIsExpiredSwap.ts
+++ b/src/features/swap/hooks/useIsExpiredSwap.ts
@@ -2,6 +2,21 @@ import { useEffect, useRef, useState } from 'react'
import type { TransactionInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { isSwapOrderTxInfo } from '@/utils/transaction-guards'
+// https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value
+const MAX_TIMEOUT = 2147483647
+
+function getExpiryDelay(expiryUnixTimestampSec: number): number {
+ const currentTimeMs = Date.now()
+ const expiryTimeMs = expiryUnixTimestampSec * 1000
+ const timeUntilExpiry = expiryTimeMs - currentTimeMs
+
+ if (timeUntilExpiry <= 0) {
+ return 0 // Already expired
+ }
+
+ return Math.min(timeUntilExpiry, MAX_TIMEOUT)
+}
+
/**
* Checks whether a swap has expired and if it hasn't it sets a timeout
* for the exact moment it will expire
@@ -14,22 +29,17 @@ const useIsExpiredSwap = (txInfo: TransactionInfo) => {
useEffect(() => {
if (!isSwapOrderTxInfo(txInfo)) return
- const checkExpiry = () => {
- const now = Date.now()
- const expiryTime = txInfo.validUntil * 1000
+ const delay = getExpiryDelay(txInfo.validUntil)
- if (now > expiryTime) {
+ if (delay === 0) {
+ setIsExpired(true)
+ } else {
+ // Set a timeout for the exact moment it will expire
+ timerRef.current = setTimeout(() => {
setIsExpired(true)
- } else {
- // Set a timeout for the exact moment it will expire
- timerRef.current = setTimeout(() => {
- setIsExpired(true)
- }, expiryTime - now)
- }
+ }, delay)
}
- checkExpiry()
-
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
diff --git a/src/features/walletconnect/components/WcConnectionState/index.tsx b/src/features/walletconnect/components/WcConnectionState/index.tsx
index e48d608677..d3f4284e72 100644
--- a/src/features/walletconnect/components/WcConnectionState/index.tsx
+++ b/src/features/walletconnect/components/WcConnectionState/index.tsx
@@ -24,12 +24,8 @@ const WcConnectionState = ({ metadata, isDelete }: { metadata?: CoreTypes.Metada
-
+
+
{isDelete ? `${name} disconnected` : `${name} successfully connected!`}