Skip to content

Commit f9dc33b

Browse files
authored
Merge pull request #1105 from ensdomains/fix/primary-name-address-mismatch
fix: verify ETH address record matches in usePrimaryName mismatch case
2 parents 73f3fd8 + 6cdf519 commit f9dc33b

File tree

3 files changed

+193
-6
lines changed

3 files changed

+193
-6
lines changed

e2e/specs/stateless/setPrimary.spec.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { expect } from '@playwright/test'
22
import { labelhash } from 'viem'
33

44
import { getResolver } from '@ensdomains/ensjs/public'
5-
import { setPrimaryName } from '@ensdomains/ensjs/wallet'
5+
import { setPrimaryName, setRecords } from '@ensdomains/ensjs/wallet'
66

77
import { test } from '../../../playwright'
88
import { createAccounts } from '../../../playwright/fixtures/accounts'
99
import {
10+
testClient,
1011
waitForTransaction,
1112
walletClient,
1213
} from '../../../playwright/fixtures/contracts/utils/addTestContracts'
@@ -487,4 +488,130 @@ test.describe('profile', () => {
487488
accounts.getAddress('user2', 5),
488489
)
489490
})
491+
492+
test('should not show primary name when ETH address record points to different address', async ({
493+
page,
494+
login,
495+
makeName,
496+
accounts,
497+
time,
498+
}) => {
499+
test.slow()
500+
501+
// Step 1: Create a name owned by user with ETH record pointing to user's own address
502+
const name = await makeName({
503+
label: 'primary-mismatch',
504+
type: 'legacy',
505+
owner: 'user',
506+
manager: 'user',
507+
addr: 'user', // Initially points to user's own address
508+
})
509+
510+
const userAddress = accounts.getAddress('user')
511+
const user2Address = accounts.getAddress('user2')
512+
513+
// Step 2: Set this name as primary name for user
514+
const tx = await setPrimaryName(walletClient, {
515+
name,
516+
account: createAccounts().getAddress('user') as `0x${string}`,
517+
})
518+
await waitForTransaction(tx)
519+
520+
await page.goto('/')
521+
await login.connect()
522+
523+
// Step 3: Verify primary name shows on user's address page
524+
await page.getByPlaceholder('Search for a name').fill(userAddress)
525+
await page.getByPlaceholder('Search for a name').press('Enter')
526+
await expect(page.getByTestId('profile-snippet')).toBeVisible({ timeout: 25000 })
527+
await expect(page.getByTestId('profile-title')).toHaveText(name)
528+
529+
// Step 4: Change the ETH address record to point to user2's address
530+
const tx2 = await setRecords(walletClient, {
531+
name,
532+
coins: [
533+
{
534+
coin: 'eth',
535+
value: user2Address,
536+
},
537+
],
538+
resolverAddress: walletClient.chain.contracts.legacyPublicResolver.address,
539+
account: accounts.getAddress('user') as `0x${string}`,
540+
})
541+
await waitForTransaction(tx2)
542+
543+
await testClient.mine({ blocks: 1 })
544+
await time.sync()
545+
546+
await page.reload()
547+
548+
// Step 5: Verify primary name NO LONGER shows on user's address page
549+
await page.getByPlaceholder('Search for a name').fill(userAddress)
550+
await page.getByPlaceholder('Search for a name').press('Enter')
551+
await expect(page.getByTestId('no-profile-snippet')).toBeVisible({ timeout: 25000 })
552+
await expect(page.getByText('No primary name set')).toBeVisible()
553+
})
554+
555+
test('name-wrapper - should not show primary name when ETH address record points to different address', async ({
556+
page,
557+
login,
558+
makeName,
559+
accounts,
560+
time,
561+
makePageObject,
562+
}) => {
563+
test.slow()
564+
565+
// Step 1: Create a name owned by user with ETH record pointing to user's own address
566+
const name = await makeName({
567+
label: 'primary-mismatch',
568+
type: 'wrapped',
569+
owner: 'user',
570+
addr: accounts.getAddress('user') as `0x${string}`,
571+
})
572+
573+
const userAddress = accounts.getAddress('user')
574+
const user2Address = accounts.getAddress('user2')
575+
576+
await page.goto('/')
577+
await login.connect()
578+
579+
const profilePage = makePageObject('ProfilePage')
580+
581+
await profilePage.goto(name)
582+
await page.getByText('Set as primary name').click()
583+
const transactionModal = makePageObject('TransactionModal')
584+
await transactionModal.autoComplete()
585+
586+
// Step 3: Verify primary name shows on user's address page
587+
await page.getByPlaceholder('Search for a name').fill(userAddress)
588+
await page.getByPlaceholder('Search for a name').press('Enter')
589+
await expect(page.getByTestId('profile-snippet')).toBeVisible({ timeout: 25000 })
590+
await expect(page.getByTestId('profile-title')).toHaveText(name)
591+
592+
// Step 4: Change the ETH address record to point to user2's address
593+
const tx2 = await setRecords(walletClient, {
594+
name,
595+
coins: [
596+
{
597+
coin: 'eth',
598+
value: user2Address,
599+
},
600+
],
601+
resolverAddress: walletClient.chain.contracts.ensPublicResolver.address as `0x${string}`,
602+
account: accounts.getAddress('user') as `0x${string}`,
603+
})
604+
await waitForTransaction(tx2)
605+
606+
await testClient.mine({ blocks: 1 })
607+
await time.sync()
608+
609+
await page.reload()
610+
611+
// Step 5: Verify primary name NO LONGER shows on user's address page
612+
await page.getByPlaceholder('Search for a name').fill(userAddress)
613+
await page.getByPlaceholder('Search for a name').press('Enter')
614+
await expect(page.getByTestId('no-profile-snippet')).toBeVisible({ timeout: 25000 })
615+
await expect(page.getByText('No primary name set')).toBeVisible()
616+
})
490617
})

src/hooks/ensjs/public/usePrimaryName.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mockFunction } from '@app/test-utils'
22

33
import { describe, expect, it, vi } from 'vitest'
44

5-
import { getName } from '@ensdomains/ensjs/public'
5+
import { getAddressRecord, getName } from '@ensdomains/ensjs/public'
66

77
import { ClientWithEns, ConfigWithEns } from '@app/types'
88

@@ -11,6 +11,7 @@ import { getPrimaryNameQueryFn } from './usePrimaryName'
1111
vi.mock('@ensdomains/ensjs/public')
1212

1313
const mockGetName = mockFunction(getName)
14+
const mockGetAddressRecord = mockFunction(getAddressRecord)
1415

1516
const address = '0xaddress'
1617
const chainId = 1
@@ -96,6 +97,14 @@ describe('getPrimaryNameQueryFn', () => {
9697
reverseResolverAddress: '0xreverseResolver',
9798
}),
9899
)
100+
// Mock getAddressRecord to return the same address so the check passes
101+
mockGetAddressRecord.mockImplementationOnce(() =>
102+
Promise.resolve({
103+
id: 60,
104+
name: 'eth',
105+
value: address,
106+
}),
107+
)
99108
const result = await getPrimaryNameQueryFn(mockConfig)({
100109
queryKey: [{ address, allowMismatch: true }, chainId, address, undefined, 'getName'],
101110
meta: {} as any,
@@ -122,6 +131,14 @@ describe('getPrimaryNameQueryFn', () => {
122131
reverseResolverAddress: '0xreverseResolver',
123132
}),
124133
)
134+
// Mock getAddressRecord to return the same address so the check passes
135+
mockGetAddressRecord.mockImplementationOnce(() =>
136+
Promise.resolve({
137+
id: 60,
138+
name: 'eth',
139+
value: address,
140+
}),
141+
)
125142
const result = await getPrimaryNameQueryFn(mockConfig)({
126143
queryKey: [{ address, allowMismatch: true }, chainId, address, undefined, 'getName'],
127144
meta: {} as any,
@@ -164,4 +181,31 @@ describe('getPrimaryNameQueryFn', () => {
164181
}
165182
`)
166183
})
184+
185+
it('should return null when allowMismatch is true but ETH address record points to different address', async () => {
186+
const differentAddress = '0x1234567890123456789012345678901234567890'
187+
188+
mockGetName.mockImplementationOnce(() =>
189+
Promise.resolve({
190+
name: 'test.eth',
191+
match: false,
192+
resolverAddress: '0xresolver',
193+
reverseResolverAddress: '0xreverseResolver',
194+
}),
195+
)
196+
// Mock getAddressRecord to return a DIFFERENT address
197+
mockGetAddressRecord.mockImplementationOnce(() =>
198+
Promise.resolve({
199+
id: 60,
200+
name: 'eth',
201+
value: differentAddress, // ← Points to different address!
202+
}),
203+
)
204+
const result = await getPrimaryNameQueryFn(mockConfig)({
205+
queryKey: [{ address, allowMismatch: true }, chainId, address, undefined, 'getName'],
206+
meta: {} as any,
207+
signal: undefined as any,
208+
})
209+
expect(result).toBeNull() // Should return null because address doesn't match
210+
})
167211
})

src/hooks/ensjs/public/usePrimaryName.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { ContractFunctionExecutionError } from 'viem'
33
import { readContract } from 'viem/actions'
44

55
import { universalResolverReverseSnippet } from '@ensdomains/ensjs/contracts'
6-
import { getName, GetNameParameters, GetNameReturnType } from '@ensdomains/ensjs/public'
6+
import {
7+
getAddressRecord,
8+
getName,
9+
GetNameParameters,
10+
GetNameReturnType,
11+
} from '@ensdomains/ensjs/public'
712
import { normalise } from '@ensdomains/ensjs/utils'
813

914
import { useQueryOptions } from '@app/hooks/useQueryOptions'
@@ -83,14 +88,13 @@ export const getPrimaryNameQueryFn =
8388
try {
8489
const normalizedVersion = normalise(originalName)
8590
isNormalized = originalName === normalizedVersion
86-
} catch (error) {
91+
} catch {
8792
// If normalisation fails, treat as non-normalized
8893
isNormalized = false
8994
}
9095
}
9196
}
92-
} catch (error) {
93-
console.error('Failed to get raw reverse name:', error)
97+
} catch {
9498
// Fall back to checking if res.name is normalized
9599
try {
96100
const normalizedVersion = normalise(res.name)
@@ -101,6 +105,18 @@ export const getPrimaryNameQueryFn =
101105
}
102106
} else {
103107
// For mismatch case, res.name is already the raw name
108+
// Check if the ETH address record for the name matches the input address
109+
try {
110+
const ethAddressRecord = await getAddressRecord(client, { name: res.name })
111+
const resolvedAddress = ethAddressRecord?.value
112+
113+
if (!resolvedAddress || resolvedAddress.toLowerCase() !== address.toLowerCase()) {
114+
return null
115+
}
116+
} catch {
117+
return null
118+
}
119+
104120
originalName = res.name
105121
isNormalized = false // Mismatches are treated as non-normalized
106122
}

0 commit comments

Comments
 (0)