Skip to content

Commit 0577d0f

Browse files
Merge branch 'main' into feature/fet-2614-add-reverse-resolving-to-referrer-paramter
2 parents 62b4293 + edafaa4 commit 0577d0f

File tree

34 files changed

+537
-204
lines changed

34 files changed

+537
-204
lines changed

.claude/settings.local.json

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,25 @@
3232
"mcp__github__list_workflow_jobs",
3333
"mcp__github__get_job_logs",
3434
"mcp__github__get_pull_request_diff",
35-
"mcp__github__get_pull_request"
35+
"mcp__github__get_pull_request",
36+
"WebFetch(domain:registry.npmjs.org)",
37+
"WebFetch(domain:github.com)"
3638
],
37-
"deny": []
39+
"deny": [
40+
"Bash(pnpm i --no-frozen-lockfile:*)",
41+
"Bash(pnpm install --no-frozen-lockfile:*)",
42+
"Bash(npm i --no-frozen-lockfile:*)",
43+
"Bash(npm install --no-frozen-lockfile:*)"
44+
]
45+
},
46+
"sandbox": {
47+
"enabled": true,
48+
"autoAllowBashIfSandboxed": true,
49+
"allowUnsandboxedCommands": false,
50+
"network": {
51+
"allowUnixSockets": [],
52+
"allowLocalBinding": true
53+
},
54+
"excludedCommands": []
3855
}
39-
}
56+
}

.github/actions/setup/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ runs:
66
- name: Install pnpm
77
uses: pnpm/action-setup@v4
88
with:
9-
version: 9.3.0
9+
version: 10.23.0
1010

1111
- name: Install Node.js
1212
uses: actions/setup-node@v4

.github/workflows/test-wallet.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ jobs:
1616
timeout-minutes: 20
1717
runs-on: ubuntu-latest
1818
container:
19-
image: mcr.microsoft.com/playwright:v1.55.0-jammy
19+
image: mcr.microsoft.com/playwright:v1.56.1-jammy
2020
steps:
2121
- uses: actions/checkout@v4
2222

2323
- name: Setup pnpm
2424
uses: pnpm/action-setup@v4
2525
with:
26-
version: 9.3.0
26+
version: 10.23.0
2727

2828
- name: Setup Node.js
2929
uses: actions/setup-node@v4

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
node_modules
55
/.pnp
66
.pnp.js
7+
.pnpm-store
78

89
# testing
910
/coverage
@@ -76,5 +77,4 @@ tsconfig.vitest-temp.json
7677
certificates
7778

7879
# Claude
79-
.claude
80-
.playwright-mcp
80+
.playwright-mcp

.husky/pre-commit

Lines changed: 0 additions & 4 deletions
This file was deleted.

.npmrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
strict-peer-dependencies=false
2-
auto-install-peers=false
2+
auto-install-peers=false
3+
public-hoist-pattern[]=*eslint*
4+
public-hoist-pattern[]=*prettier*
5+
public-hoist-pattern[]=*stylelint*

NOTES_BEFORE_RELEASE.md

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { expect } from '@playwright/test'
2+
import type { Page } from '@playwright/test'
3+
4+
import { setPrimaryName } from '@ensdomains/ensjs/wallet'
5+
6+
import { test } from '../../../playwright'
7+
import { createAccounts } from '../../../playwright/fixtures/accounts'
8+
import {
9+
waitForTransaction,
10+
walletClient,
11+
} from '../../../playwright/fixtures/contracts/utils/addTestContracts'
12+
import type { Login } from '../../../playwright/fixtures/login'
13+
14+
// Constants
15+
const INVALID_RESOLVER = '0x0000000000000000000000000000000000000001'
16+
const UNIVERSAL_RESOLVER_RESOLVE_SELECTOR = '0x82ad56cb' // resolve(bytes,bytes)
17+
const NAMES_LIST_TIMEOUT = 15000
18+
const RETRY_TIMEOUT = 45000
19+
20+
// Helper to get user address consistently
21+
const getUserAddress = () => createAccounts().getAddress('user') as `0x${string}`
22+
23+
// Helper to set primary name and wait for transaction
24+
async function setPrimaryNameAndWait(name: string, userAddress: `0x${string}`) {
25+
const tx = await setPrimaryName(walletClient, {
26+
name,
27+
account: userAddress,
28+
})
29+
await waitForTransaction(tx)
30+
}
31+
32+
// Helper to create a name owned by user
33+
async function createUserName(makeName: any, label: string) {
34+
return makeName({
35+
label,
36+
type: 'legacy',
37+
owner: 'user',
38+
manager: 'user',
39+
addr: 'user',
40+
})
41+
}
42+
43+
// Helper to set an invalid resolver that triggers ContractFunctionExecutionError
44+
async function setInvalidResolver(
45+
page: Page,
46+
name: string,
47+
login: InstanceType<typeof Login>,
48+
makePageObject: any,
49+
) {
50+
const morePage = makePageObject('MorePage')
51+
const transactionModal = makePageObject('TransactionModal')
52+
53+
await morePage.goto(name)
54+
await login.connect()
55+
56+
await morePage.editResolverButton.click()
57+
await page.getByTestId('custom-resolver-radio').check()
58+
await page.getByTestId('dogfood').fill(INVALID_RESOLVER)
59+
await page.getByTestId('update-button').click()
60+
await transactionModal.autoComplete()
61+
}
62+
63+
// Helper to assert address page loaded gracefully (doesn't crash)
64+
async function assertAddressPageLoaded(page: Page) {
65+
await page.waitForLoadState('networkidle')
66+
67+
const hasProfile = await page
68+
.getByTestId('profile-snippet')
69+
.isVisible()
70+
.catch(() => false)
71+
const hasNoProfile = await page
72+
.getByTestId('no-profile-snippet')
73+
.isVisible()
74+
.catch(() => false)
75+
76+
expect(hasProfile || hasNoProfile).toBeTruthy()
77+
}
78+
79+
// Helper to intercept and fail RPC calls to test retry logic
80+
async function setupRPCInterception(page: Page, maxFailures: number): Promise<() => number> {
81+
let requestCount = 0
82+
83+
await page.route('**/*', async (route) => {
84+
const request = route.request()
85+
const url = request.url()
86+
87+
if (url.includes(':8545') || url.includes('localhost:8545')) {
88+
try {
89+
const postData = request.postDataJSON()
90+
91+
if (postData?.method === 'eth_call') {
92+
const data = postData?.params?.[0]?.data
93+
94+
if (data && data.startsWith(UNIVERSAL_RESOLVER_RESOLVE_SELECTOR)) {
95+
requestCount += 1
96+
97+
if (requestCount <= maxFailures) {
98+
// Simulate network error (not ContractFunctionExecutionError)
99+
await route.abort('failed')
100+
return
101+
}
102+
}
103+
}
104+
} catch (e) {
105+
// If we can't parse the request, continue normally
106+
}
107+
}
108+
109+
await route.continue()
110+
})
111+
112+
return () => requestCount
113+
}
114+
115+
test.describe('Address page error handling', () => {
116+
// Tests for usePrimaryName.ts fix that catches ContractFunctionExecutionError
117+
// and returns null (graceful failure) while still throwing other errors for retry
118+
119+
test('should load address page without crashing when getName throws ContractFunctionExecutionError', async ({
120+
page,
121+
login,
122+
makeName,
123+
makePageObject,
124+
}) => {
125+
test.slow()
126+
127+
// SETUP: Create name with invalid resolver + set as primary name
128+
const name = await createUserName(makeName, 'error-test-name')
129+
await setInvalidResolver(page, name, login, makePageObject)
130+
131+
const profilePage = makePageObject('ProfilePage')
132+
const transactionModal = makePageObject('TransactionModal')
133+
await profilePage.goto(name)
134+
await page.getByText('Set as primary name').click()
135+
await transactionModal.autoComplete()
136+
137+
// ACTION: Navigate to address page (triggers getName with invalid resolver)
138+
const userAddress = getUserAddress()
139+
await page.goto(`/${userAddress}`)
140+
141+
// ASSERT: Page loads gracefully without crashing
142+
await assertAddressPageLoaded(page)
143+
144+
// ASSERT: Other functionality still works (names list loads)
145+
await expect(page.getByTestId('names-list')).toBeVisible({ timeout: NAMES_LIST_TIMEOUT })
146+
})
147+
148+
test('should retry on non-ContractFunctionExecutionError and not return null early', async ({
149+
page,
150+
login,
151+
makeName,
152+
}) => {
153+
test.slow()
154+
155+
// SETUP: Create valid name and set as primary name
156+
const name = await createUserName(makeName, 'retry-test-name')
157+
const userAddress = getUserAddress()
158+
await setPrimaryNameAndWait(name, userAddress)
159+
160+
// SETUP: Intercept RPC calls to simulate network failures
161+
const maxFailures = 2
162+
const getRequestCount = await setupRPCInterception(page, maxFailures)
163+
164+
// ACTION: Navigate to address page (triggers getName with simulated failures)
165+
await page.goto(`/${userAddress}`)
166+
await login.connect()
167+
168+
// ASSERT: Page eventually loads after retries
169+
await page.waitForLoadState('networkidle')
170+
await expect(page.getByTestId('profile-snippet')).toBeVisible({ timeout: RETRY_TIMEOUT })
171+
await expect(page.getByTestId('profile-snippet-name')).toContainText(name)
172+
173+
// ASSERT: Retries occurred (proves non-ContractFunctionExecutionError thrown, not caught)
174+
const requestCount = getRequestCount()
175+
expect(requestCount).toBeGreaterThan(maxFailures)
176+
expect(requestCount).toBeGreaterThanOrEqual(3) // At least 2 failures + 1 success
177+
})
178+
179+
test.afterEach(async () => {
180+
// Clean up: reset primary name after each test
181+
await setPrimaryName(walletClient, {
182+
name: '',
183+
account: getUserAddress(),
184+
}).catch(() => {
185+
// Ignore errors during cleanup
186+
})
187+
})
188+
})

e2e/specs/stateless/createSubname.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,45 @@ test('should allow skipping records when creating a subname', async ({
350350
await expect(page).toHaveURL(new RegExp(`/${subname}`), { timeout: 15000 })
351351
await expect(page.getByTestId('profile-empty-banner')).toBeVisible()
352352
})
353+
354+
test('should persist referrer parameter after creating a subname and redirecting to subname profile', async ({
355+
page,
356+
login,
357+
makeName,
358+
makePageObject,
359+
}) => {
360+
test.slow()
361+
362+
const name = await makeName({
363+
label: 'referrer-subname-test',
364+
type: 'wrapped',
365+
owner: 'user',
366+
})
367+
368+
const referrer = '0x1234567890123456789012345678901234567890'
369+
const subnamesPage = makePageObject('SubnamesPage')
370+
const transactionModal = makePageObject('TransactionModal')
371+
372+
// Navigate to subnames page with referrer in URL
373+
await page.goto(`/${name}?tab=subnames&referrer=${referrer}`)
374+
await login.connect()
375+
376+
// Verify referrer is in URL
377+
expect(page.url()).toContain(`referrer=${referrer}`)
378+
379+
// Create a new subname without adding records
380+
await subnamesPage.getAddSubnameButton.click()
381+
await subnamesPage.getAddSubnameInput.fill('test')
382+
await subnamesPage.getSubmitSubnameButton.click()
383+
await page.getByTestId('create-subname-profile-next').click()
384+
385+
// Complete the transaction
386+
await transactionModal.autoComplete()
387+
388+
// After transaction completes, the app should redirect to the subname profile
389+
const subname = `test.${name}`
390+
await expect(page).toHaveURL(new RegExp(`/${subname}`), { timeout: 15000 })
391+
392+
// Verify referrer is still in the URL after redirect
393+
expect(page.url()).toContain(`referrer=${referrer}`)
394+
})

e2e/specs/stateless/extendNames.spec.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,9 @@ test('should be able to extend a single wrapped name using deep link', async ({
676676
await extendNamesModal.getExtendButton.click()
677677
await transactionModal.autoComplete()
678678
const newTimestamp = await profilePage.getExpiryTimestamp()
679-
await expect(newTimestamp).toEqual(timestamp + 31536000000)
679+
const difference = newTimestamp - timestamp
680+
// Allow 1 day tolerance for timezone/calendar day boundary differences
681+
expect(Math.abs(difference - 31536000000)).toBeLessThanOrEqual(86400000)
680682
})
681683
})
682684

@@ -945,13 +947,30 @@ test.describe('Wrapped Name Renewal with Referrer', () => {
945947
})
946948

947949
const transactionModal = makePageObject('TransactionModal')
948-
949-
const referrerAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'
950+
const referrerAddress = '0x1234567890123456789012345678901234567890'
950951

951952
// Start on profile page with referrer
952953
await page.goto(`/${name}?referrer=${referrerAddress}`)
953954
await login.connect()
954955

956+
// Navigate to different profile tabs
957+
await page.getByTestId('records-tab').click()
958+
await page.waitForTimeout(500)
959+
expect(page.url()).toContain(`referrer=${referrerAddress}`)
960+
961+
await page.getByTestId('ownership-tab').click()
962+
await page.waitForTimeout(500)
963+
expect(page.url()).toContain(`referrer=${referrerAddress}`)
964+
965+
await page.getByTestId('subnames-tab').click()
966+
await page.waitForTimeout(500)
967+
expect(page.url()).toContain(`referrer=${referrerAddress}`)
968+
969+
// Navigate back to profile page
970+
await page.getByTestId('profile-tab').click()
971+
await page.waitForTimeout(500)
972+
expect(page.url()).toContain(`referrer=${referrerAddress}`)
973+
955974
// Navigate through the renewal flow
956975
await page.getByTestId('extend-button').click()
957976

0 commit comments

Comments
 (0)