Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/explorer/src/api/operator/operatorApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ export { getAccountOrders } from './accountOrderUtils'
const ENV_REQUEST_TIMEOUT_MS = 12_000

function withBarnTimeout<T>(promise: Promise<T>, operation: string): Promise<T> {
return withTimeout(promise, ENV_REQUEST_TIMEOUT_MS, `${operation}: BARN`)
return withTimeout(promise, {
timeout: ENV_REQUEST_TIMEOUT_MS,
timeoutMessage: `${operation}: BARN. Timeout after ${ENV_REQUEST_TIMEOUT_MS} ms`,
})
}

function withProdTimeout<T>(promise: Promise<T>, operation: string): Promise<T> {
return withTimeout(promise, ENV_REQUEST_TIMEOUT_MS, `${operation}: PROD`)
return withTimeout(promise, {
timeout: ENV_REQUEST_TIMEOUT_MS,
timeoutMessage: `${operation}: PROD. Timeout after ${ENV_REQUEST_TIMEOUT_MS} ms`,
})
}

/**
Expand Down
1 change: 0 additions & 1 deletion libs/common-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export * from './misc'
export * from './node'
export * from './normalizeRawAmount'
export * from './parseENSAddress'
export * from './promiseWithTimeout'
export * from './rawToTokenAmount'
export * from './request'
export * from './resolveENSContentHash'
Expand Down
23 changes: 22 additions & 1 deletion libs/common-utils/src/misc.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
import { isRejectRequestProviderError } from './misc'
import { isRejectRequestProviderError, TimeoutError, withTimeout } from './misc'

describe('withTimeout', () => {
it('resolves when the promise settles before the timeout', async () => {
await expect(withTimeout(Promise.resolve('ok'), { timeout: 1000, timeoutMessage: 'Timed out' })).resolves.toBe('ok')
})

it('rejects with TimeoutError when the promise times out', async () => {
await expect(
withTimeout(new Promise(() => undefined), { timeout: 50, timeoutMessage: 'Timed out' }),
).rejects.toThrow(TimeoutError)
})

it('rejects with the provided timeout message', async () => {
await expect(
withTimeout(new Promise(() => undefined), {
timeout: 50,
timeoutMessage: 'Timed out',
}),
).rejects.toThrow('Timed out')
})
})

describe('isRejectRequestProviderError', () => {
it('detects the standard EIP-1193 rejection code', () => {
Expand Down
23 changes: 18 additions & 5 deletions libs/common-utils/src/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,26 @@ export function isPromiseFulfilled<T>(
return promiseResult.status === 'fulfilled'
}

export function withTimeout<T>(promise: Promise<T>, ms: number, context?: string): Promise<T> {
const failOnTimeout = delay(ms).then(() => {
const errorMessage = 'Timeout after ' + ms + ' ms'
throw new Error(context ? `${context}. ${errorMessage}` : errorMessage)
export class TimeoutError extends Error {}

interface TimeoutOptions {
timeout: number
timeoutMessage: string
}

export async function withTimeout<T>(promise: Promise<T>, options: TimeoutOptions): Promise<T> {
const { timeout, timeoutMessage } = options
let timeoutId: ReturnType<typeof setTimeout> | undefined

const failOnTimeout = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new TimeoutError(timeoutMessage)), timeout)
})

return Promise.race([promise, failOnTimeout])
try {
return await Promise.race([promise, failOnTimeout])
} finally {
if (timeoutId) clearTimeout(timeoutId)
}
}

type WindowWithMapping = Window & typeof globalThis & Record<string, unknown>
Expand Down
35 changes: 0 additions & 35 deletions libs/common-utils/src/promiseWithTimeout.test.ts

This file was deleted.

44 changes: 0 additions & 44 deletions libs/common-utils/src/promiseWithTimeout.ts

This file was deleted.

103 changes: 78 additions & 25 deletions libs/wallet/src/api/state/walletCapabilitiesAtom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const writableIsSafeWalletAtom = isSafeWalletAtom as WritableAtom<boolean, [bool

import type { WalletCapabilities } from './walletCapabilitiesAtom'
import type { WalletInfo } from '../types'
import type { EIP1193Provider } from 'viem'
import type { EIP1193Provider, PublicClient } from 'viem'
import type { Connector } from 'wagmi'

jest.mock('../../wagmi/state/walletMetadata.atoms', () => {
Expand All @@ -53,25 +53,22 @@ const MOCK_ACCOUNT = '0x1234567890123456789012345678901234567890' as const
const MOCK_CHAIN_ID = SupportedChainId.MAINNET
const MOCK_CONNECTOR = { type: 'injected' } as Connector

const mockLogWalletWarn = jest.fn()
const mockGetCapabilities = jest.fn()
const mockWagmiConfigGetClient = jest.fn()
const mockGetIsWalletConnect = jest.fn()
const mockIsMobile = { value: false }

jest.mock('@cowprotocol/common-utils', () => ({
getCurrentChainIdFromUrl: () => 1,
get isMobile() {
return mockIsMobile.value
},
logWallet: {
warn: (...args: unknown[]) => mockLogWalletWarn(...args),
},
PromiseWithTimeout: <T>(_: number, handler: (resolve: (value: T) => void) => void): Promise<T> =>
new Promise<T>((resolve) => {
handler(resolve)
}),
}))
jest.mock('@cowprotocol/common-utils', () => {
const actual = jest.requireActual<typeof import('@cowprotocol/common-utils')>('@cowprotocol/common-utils')

return {
...actual,
getCurrentChainIdFromUrl: () => 1,
get isMobile() {
return mockIsMobile.value
},
}
})

jest.mock('../../wagmi/hooks/useIsWalletConnect', () => ({
getIsWalletConnect: (...args: unknown[]) => mockGetIsWalletConnect(...args),
Expand Down Expand Up @@ -101,7 +98,7 @@ function setWalletInfo(
account: string
chainId: SupportedChainId
connector: Connector
provider: EIP1193Provider
provider: EIP1193Provider | PublicClient
}> = {},
): void {
store.set(walletInfoAtom, {
Expand Down Expand Up @@ -303,21 +300,34 @@ describe('walletCapabilitiesAtom', () => {
})
})

it('returns empty object when getCapabilities does not settle before timeout', async () => {
it('returns null when getCapabilities fails and provider does not support EIP-1193 requests', async () => {
const error = new Error('viem error')
mockGetCapabilities.mockRejectedValue(error)

const store = createStore()
setWalletInfo(store, { provider: {} as PublicClient })

const result = await store.get(walletCapabilitiesAtom)

expect(result).toBeNull()
})

it('returns null when getCapabilities times out and provider does not support EIP-1193 requests', async () => {
jest.useFakeTimers()
mockGetCapabilities.mockImplementation(() => new Promise(() => undefined))

const store = createStore()
setWalletInfo(store)

const resultPromise = store.get(walletCapabilitiesAtom)
await jest.advanceTimersByTimeAsync(5_000)
const result = await resultPromise
setWalletInfo(store, { provider: {} as PublicClient })

expect(result).toEqual({})
expect(mockLogWalletWarn).toHaveBeenCalledWith(expect.stringContaining('Wallet capabilities loading timed out'))
try {
const resultPromise = store.get(walletCapabilitiesAtom)
await jest.advanceTimersByTimeAsync(5_000)
const result = await resultPromise

jest.useRealTimers()
expect(result).toBeNull()
} finally {
jest.useRealTimers()
}
})
})

Expand Down Expand Up @@ -347,6 +357,49 @@ describe('walletCapabilitiesAtom', () => {

expect(result).toEqual(capabilities)
})

it('uses legacy fallback when getCapabilities times out', async () => {
jest.useFakeTimers()
const capabilities: WalletCapabilities = { atomic: { status: 'supported' } }
mockGetCapabilities.mockImplementation(() => new Promise(() => undefined))
const mockRequest = jest.fn().mockResolvedValue({ '0x1': capabilities })

const store = createStore()
setWalletInfo(store, { provider: createMockEip1193Provider(mockRequest) })

try {
const resultPromise = store.get(walletCapabilitiesAtom)
await jest.advanceTimersByTimeAsync(5_000)
const result = await resultPromise

expect(result).toEqual(capabilities)
expect(mockRequest).toHaveBeenCalledWith({
method: 'wallet_getCapabilities',
params: [MOCK_ACCOUNT],
})
} finally {
jest.useRealTimers()
}
})

it('returns null when legacy fallback times out', async () => {
jest.useFakeTimers()
mockGetCapabilities.mockRejectedValue(new Error('viem error'))
const mockRequest = jest.fn().mockImplementation(() => new Promise(() => undefined))

const store = createStore()
setWalletInfo(store, { provider: createMockEip1193Provider(mockRequest) })

try {
const resultPromise = store.get(walletCapabilitiesAtom)
await jest.advanceTimersByTimeAsync(5_000)
const result = await resultPromise

expect(result).toBeNull()
} finally {
jest.useRealTimers()
}
})
})
})

Expand Down
Loading
Loading