Skip to content

Commit a4dd528

Browse files
refactor: move resolution logic into hook and add loading indicators
Move ENS name resolution logic from utility function into useResolvedReferrer hook to follow React Query semantics. Add loading indicators to buttons in ExtendNames, DesyncedMessage, and Pricing flows while referrer resolves. - Remove resolveReferrer utility, keep only getReferrerHex - Inline resolution logic in hook's query function - Show loading spinner on buttons during referrer resolution - Update tests to mock getAddressRecord directly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 486db07 commit a4dd528

File tree

8 files changed

+67
-164
lines changed

8 files changed

+67
-164
lines changed

src/components/@molecules/DesyncedMessage/DesyncedMessage.test.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('DesyncedMessage', () => {
3131
mockUseAccount.mockReturnValue({ isConnected: true })
3232
mockUseReferrer.mockReturnValue(undefined)
3333
mockUseResolvedReferrer.mockReturnValue({
34-
data: null,
34+
data: undefined,
3535
isLoading: false,
3636
isError: false,
3737
error: null,
@@ -45,7 +45,7 @@ describe('DesyncedMessage', () => {
4545
it('should disable action button when referrer is resolving', () => {
4646
mockUseReferrer.mockReturnValue('vitalik.eth')
4747
mockUseResolvedReferrer.mockReturnValue({
48-
data: null,
48+
data: undefined,
4949
isLoading: true,
5050
isError: false,
5151
error: null,
@@ -54,7 +54,7 @@ describe('DesyncedMessage', () => {
5454
render(
5555
<DesyncedMessage
5656
name="test.eth"
57-
expiryDate={new Date(Date.now() - 1000)} // Past date - no minSeconds
57+
expiryDate={new Date(Date.now() - 1000)}
5858
isGracePeriod={false}
5959
/>,
6060
)
@@ -77,7 +77,7 @@ describe('DesyncedMessage', () => {
7777
render(
7878
<DesyncedMessage
7979
name="test.eth"
80-
expiryDate={new Date(Date.now() - 1000)} // Past date - no minSeconds
80+
expiryDate={new Date(Date.now() - 1000)}
8181
isGracePeriod={false}
8282
/>,
8383
)
@@ -97,7 +97,6 @@ describe('DesyncedMessage', () => {
9797
error: null,
9898
})
9999

100-
// Set expiryDate far in the future so minSeconds calculates to 0 (triggers createTransactionFlow)
101100
const futureDate = new Date(Date.now() + ONE_DAY * 1000 + 10000)
102101

103102
render(
@@ -127,13 +126,12 @@ describe('DesyncedMessage', () => {
127126
it('should pass undefined referrer when no referrer is resolved', async () => {
128127
mockUseReferrer.mockReturnValue(undefined)
129128
mockUseResolvedReferrer.mockReturnValue({
130-
data: null,
129+
data: undefined,
131130
isLoading: false,
132131
isError: false,
133132
error: null,
134133
})
135134

136-
// Set expiryDate far in the future so minSeconds calculates to 0 (triggers createTransactionFlow)
137135
const futureDate = new Date(Date.now() + ONE_DAY * 1000 + 10000)
138136

139137
render(

src/components/@molecules/DesyncedMessage/DesyncedMessage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const DesyncedMessage = ({
5353
children: t('banner.desynced.action'),
5454
colorStyle: 'redPrimary',
5555
disabled: isReferrerResolving,
56+
loading: isReferrerResolving,
5657
onClick: () => {
5758
if (!name) return
5859
const minSeconds = calculateMinSeconds(expiryDate)
@@ -69,7 +70,7 @@ export const DesyncedMessage = ({
6970
transactions: [
7071
createTransactionItem('repairDesyncedName', {
7172
name,
72-
referrer: resolvedReferrer ?? undefined,
73+
referrer: resolvedReferrer,
7374
hasWrapped: true,
7475
}),
7576
],

src/hooks/useResolvedReferrer.test.tsx

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,83 +2,86 @@ import { renderHook, waitFor } from '@app/test-utils'
22
import { beforeEach, describe, expect, it, vi } from 'vitest'
33
import { Address } from 'viem'
44

5-
import { resolveReferrer } from '@app/utils/referrer'
6-
7-
vi.mock('@app/utils/referrer', () => ({
8-
resolveReferrer: vi.fn(),
9-
}))
5+
import { getAddressRecord } from '@ensdomains/ensjs/public'
6+
import { EMPTY_BYTES32 } from '@ensdomains/ensjs/utils'
107

118
import { useResolvedReferrer } from './useResolvedReferrer'
129

10+
vi.mock('@ensdomains/ensjs/public', () => ({
11+
getAddressRecord: vi.fn(),
12+
}))
13+
1314
describe('useResolvedReferrer', () => {
1415
beforeEach(() => {
1516
vi.clearAllMocks()
1617
})
1718

18-
it('should return null when no referrer provided', () => {
19+
it('should return undefined when no referrer provided', () => {
1920
const { result } = renderHook(() => useResolvedReferrer({ referrer: undefined }))
2021

21-
expect(result.current.data).toBeNull()
22+
expect(result.current.data).toBeUndefined()
2223
expect(result.current.isLoading).toBe(false)
2324
})
2425

2526
it('should resolve ENS name to hex', async () => {
26-
const mockHex = '0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045' as Address
27-
vi.mocked(resolveReferrer).mockResolvedValueOnce(mockHex)
27+
const mockAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Address
28+
vi.mocked(getAddressRecord).mockResolvedValueOnce({
29+
value: mockAddress,
30+
coin: 60,
31+
} as any)
2832

2933
const { result } = renderHook(() => useResolvedReferrer({ referrer: 'vitalik.eth' }))
3034

3135
await waitFor(() => {
32-
expect(result.current.data).toBe(mockHex)
36+
expect(result.current.data).toBeDefined()
37+
expect(result.current.data?.length).toBe(66)
3338
expect(result.current.isLoading).toBe(false)
3439
})
3540

36-
expect(vi.mocked(resolveReferrer)).toHaveBeenCalledWith(expect.anything(), 'vitalik.eth')
41+
expect(vi.mocked(getAddressRecord)).toHaveBeenCalled()
3742
})
3843

3944
it('should handle resolution errors gracefully', async () => {
40-
vi.mocked(resolveReferrer).mockRejectedValueOnce(new Error('Resolution failed'))
45+
vi.mocked(getAddressRecord).mockRejectedValueOnce(new Error('Resolution failed'))
4146

4247
const { result } = renderHook(() => useResolvedReferrer({ referrer: 'invalid.eth' }))
4348

4449
await waitFor(() => {
45-
expect(result.current.data).toBeNull()
50+
expect(result.current.data).toBeUndefined()
4651
expect(result.current.isError).toBe(true)
4752
})
4853
})
4954

50-
it('should handle null resolution result', async () => {
51-
vi.mocked(resolveReferrer).mockResolvedValueOnce(null)
55+
it('should return EMPTY_BYTES32 when name has no address', async () => {
56+
vi.mocked(getAddressRecord).mockResolvedValueOnce(null)
5257

5358
const { result } = renderHook(() => useResolvedReferrer({ referrer: 'nonexistent.eth' }))
5459

5560
await waitFor(() => {
56-
expect(result.current.data).toBeNull()
61+
expect(result.current.data).toBe(EMPTY_BYTES32)
5762
expect(result.current.isLoading).toBe(false)
5863
})
5964
})
6065

61-
it('should return null when referrer is empty string', () => {
66+
it('should return undefined when referrer is empty string', () => {
6267
const { result } = renderHook(() => useResolvedReferrer({ referrer: '' }))
6368

64-
expect(result.current.data).toBeNull()
69+
expect(result.current.data).toBeUndefined()
6570
expect(result.current.isLoading).toBe(false)
66-
expect(vi.mocked(resolveReferrer)).not.toHaveBeenCalled()
71+
expect(vi.mocked(getAddressRecord)).not.toHaveBeenCalled()
6772
})
6873

69-
it('should resolve hex address to padded 32-byte hex', async () => {
74+
it('should resolve hex address without calling getAddressRecord', async () => {
7075
const hexAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
71-
const paddedHex =
72-
'0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045' as Address
73-
vi.mocked(resolveReferrer).mockResolvedValueOnce(paddedHex)
7476

7577
const { result } = renderHook(() => useResolvedReferrer({ referrer: hexAddress }))
7678

7779
await waitFor(() => {
78-
expect(result.current.data).toBe(paddedHex)
80+
expect(result.current.data?.length).toBe(66)
7981
expect(result.current.isLoading).toBe(false)
8082
})
8183

82-
expect(vi.mocked(resolveReferrer)).toHaveBeenCalledWith(expect.anything(), hexAddress)
84+
// Should not call getAddressRecord for hex addresses
85+
expect(vi.mocked(getAddressRecord)).not.toHaveBeenCalled()
8386
})
8487
})

src/hooks/useResolvedReferrer.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { QueryFunctionContext } from '@tanstack/react-query'
2-
import { Hex } from 'viem'
2+
import { Hex, isHex } from 'viem'
3+
4+
import { getAddressRecord } from '@ensdomains/ensjs/public'
5+
import { EMPTY_BYTES32 } from '@ensdomains/ensjs/utils'
36

47
import { useQueryOptions } from '@app/hooks/useQueryOptions'
58
import { ConfigWithEns, CreateQueryKey, QueryConfig } from '@app/types'
69
import { prepareQueryOptions } from '@app/utils/prepareQueryOptions'
710
import { useQuery } from '@app/utils/query/useQuery'
8-
import { resolveReferrer } from '@app/utils/referrer'
11+
import { getReferrerHex } from '@app/utils/referrer'
912

1013
type UseResolvedReferrerParameters = {
1114
referrer?: string
1215
}
1316

14-
type UseResolvedReferrerReturnType = Hex | null
17+
type UseResolvedReferrerReturnType = Hex | undefined
1518

1619
type UseResolvedReferrerConfig = QueryConfig<UseResolvedReferrerReturnType, Error>
1720

@@ -24,23 +27,17 @@ const resolveReferrerQueryFn =
2427
async <TParams extends UseResolvedReferrerParameters>({
2528
queryKey: [{ referrer }, chainId],
2629
}: QueryFunctionContext<UseResolvedReferrerQueryKey<TParams>>) => {
27-
if (!referrer) return null
30+
if (!referrer) return EMPTY_BYTES32
31+
32+
if (isHex(referrer)) {
33+
return getReferrerHex(referrer)
34+
}
2835

2936
const client = config.getClient({ chainId })
30-
return resolveReferrer(client, referrer)
37+
const addressRecord = await getAddressRecord(client, { name: referrer })
38+
return addressRecord?.value ? getReferrerHex(addressRecord.value) : EMPTY_BYTES32
3139
}
3240

33-
/**
34-
* Hook to resolve a referrer (ENS name or hex address) to a 32-byte hex value.
35-
*
36-
* For ENS names, attempts to:
37-
* 1. Get ETH address record from the name
38-
* 2. Fall back to owner/registrant if no address record
39-
* 3. Convert resolved address to 32-byte hex
40-
*
41-
* @param referrer - ENS name or hex address to resolve
42-
* @returns Query result with resolved hex value
43-
*/
4441
export const useResolvedReferrer = <TParams extends UseResolvedReferrerParameters>({
4542
enabled = true,
4643
gcTime,
@@ -72,7 +69,7 @@ export const useResolvedReferrer = <TParams extends UseResolvedReferrerParameter
7269
const query = useQuery(preparedOptions)
7370

7471
return {
75-
data: query.data ?? null,
72+
data: query.data,
7673
isLoading: query.isLoading,
7774
isError: query.isError,
7875
error: query.error,

src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('Extendnames', () => {
6868
mockUseExpiry.mockReturnValue({ data: { expiry: { date: new Date() } }, isLoading: false })
6969
mockUseReferrer.mockReturnValue(undefined)
7070
mockUseResolvedReferrer.mockReturnValue({
71-
data: null,
71+
data: undefined,
7272
isLoading: false,
7373
isError: false,
7474
error: null,
@@ -152,7 +152,7 @@ describe('Extendnames', () => {
152152
it('should handle failed referrer resolution gracefully', async () => {
153153
mockUseReferrer.mockReturnValueOnce('invalid.eth')
154154
mockUseResolvedReferrer.mockReturnValueOnce({
155-
data: null,
155+
data: undefined,
156156
isLoading: false,
157157
isError: true,
158158
error: new Error('Resolution failed'),
@@ -172,10 +172,10 @@ describe('Extendnames', () => {
172172
expect(screen.getByTestId('extend-names-modal')).toBeInTheDocument()
173173
})
174174

175-
it('should show loading state while referrer is resolving', () => {
175+
it('should show disabled button but display pricing content while referrer is resolving', () => {
176176
mockUseReferrer.mockReturnValueOnce('vitalik.eth')
177177
mockUseResolvedReferrer.mockReturnValueOnce({
178-
data: null,
178+
data: undefined,
179179
isLoading: true,
180180
isError: false,
181181
error: null,
@@ -191,7 +191,11 @@ describe('Extendnames', () => {
191191
/>,
192192
)
193193

194-
// Should be in loading state when referrer is resolving
195-
expect(screen.queryByTestId('extend-names-modal')).toBeInTheDocument()
194+
// Pricing content should be visible (Invoice is rendered)
195+
expect(screen.getByText('Invoice')).toBeInTheDocument()
196+
197+
// Next button should be disabled while referrer is resolving
198+
const trailingButton = screen.getByTestId('extend-names-confirm')
199+
expect(trailingButton).toHaveAttribute('disabled')
196200
})
197201
})

src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ const ExtendNames = ({
234234
duration: seconds,
235235
names,
236236
startDateTimestamp: expiryDate?.getTime(),
237-
referrer: resolvedReferrer ?? undefined,
237+
referrer: resolvedReferrer,
238238
hasWrapped,
239239
},
240240
stateOverride: [
@@ -286,12 +286,9 @@ const ExtendNames = ({
286286
const view = flow[viewIdx]
287287

288288
const isBaseDataLoading =
289-
!isAccountConnected ||
290-
isBalanceLoading ||
291-
isExpiryEnabledAndLoading ||
292-
isEthPriceLoading ||
293-
isReferrerResolving
294-
const isRegisterLoading = isPriceLoading || (isEstimateGasLoading && !estimateGasLimitError)
289+
!isAccountConnected || isBalanceLoading || isExpiryEnabledAndLoading || isEthPriceLoading
290+
const isRegisterLoading =
291+
isPriceLoading || (isEstimateGasLoading && !estimateGasLimitError) || isReferrerResolving
295292

296293
const { title, alert, buttonProps } = match(view)
297294
.with('no-ownership-warning', () => ({
@@ -318,6 +315,7 @@ const ExtendNames = ({
318315
alert: undefined,
319316
buttonProps: {
320317
disabled: isRegisterLoading,
318+
loading: isRegisterLoading,
321319
onClick: () => {
322320
if (!totalRentFee) return
323321
const transactions = createTransactionItem('extendNames', {
@@ -330,7 +328,7 @@ const ExtendNames = ({
330328
bufferPercentage: CURRENCY_FLUCTUATION_BUFFER_PERCENTAGE,
331329
currency: userConfig.currency === 'fiat' ? 'usd' : 'eth',
332330
}),
333-
referrer: resolvedReferrer ?? undefined,
331+
referrer: resolvedReferrer,
334332
hasWrapped,
335333
})
336334
dispatch({ name: 'setTransactions', payload: [transactions] })

0 commit comments

Comments
 (0)