Implements subdomain management and ENS renewal flow#56
Implements subdomain management and ENS renewal flow#56
Conversation
…s and subdomain profiles - Added `FounderSubdomainCard` to manage subdomain assignments for founders. - Introduced `SubdomainProfileCard` for setting traits and primary names for subdomains. - Updated `ExpiryExtensionCard` to include additional parameters for renewal proposals. - Refactored `EnsTraitsCard` to support new ENS traits and improved state management. - Enhanced blockchain management functions to support batch subdomain creation and renewal transactions. - Updated tests to cover new functionalities and ensure robustness.
- Added `CompanyCardFounders` to display founder subdomains and manage their assignments. - Refactored `CompanyCard` to support a new slot for rendering founder information. - Removed the deprecated `FounderSubdomainCard` to streamline the component structure. - Updated component exports to include the new `CompanyCardFounders` for better modularity.
- Added `ManagementTabs` component for better organization of management sections. - Refactored `RegistrationStatusCard` to `RegistrationStatusInline` for improved inline display of registration status. - Introduced `formatDate` utility function for consistent date formatting across components. - Enhanced `CompanyCard` to include a status slot for displaying registration status. - Updated styles in various components for improved UI consistency and responsiveness. - Removed deprecated components and streamlined exports for better modularity.
- Updated `vitest.config.ts` to include path aliasing for easier imports. - Modified `system-flow.md` to reflect the latest updates and clarify Safe proposal authentication rules. - Refactored `EnsDashboardPage` to remove the deprecated `CompanyCardFounders` and streamline the component structure. - Introduced `SubdomainManagerCard` tests to ensure proper rendering and functionality of subdomain management features. - Added utility functions for wallet handling and improved Safe wallet integration in `use-safe-wallet`. - Enhanced error handling in the Safe proposal API to provide clearer feedback on authorization issues.
Remove MessageCircle import, LogOut import, avatarFallback variable, unused name destructure, and unused catch binding.
…n card - Introduced `getEnsRenewalQuoteAction` to fetch renewal quotes for ENS names. - Updated `ExpiryExtensionCard` to utilize the new quote functionality, allowing users to propose renewals based on the latest quotes. - Enhanced state management for renewal values, including manual overrides and error handling. - Added tests for the new renewal quote logic and state management functions. - Removed the deprecated `CompanyCardFounders` component to streamline the dashboard structure.
- Introduced tests for the ExpiryExtensionCard to ensure proper rendering and functionality. - Verified that the component renders without errors and displays the expected elements based on the renewal quote state. - Mocked necessary hooks and dependencies to isolate component behavior during testing.
…oard - Introduced the `FounderSubdomainList` component to manage and display subdomains for founders, including their roles and equity. - Updated `EnsDashboardPage` to include the new `FounderSubdomainList`, enhancing the management of founder subdomains. - Refactored `ManagementTabs` to rename the 'Team' tab to 'Subdomains' for better clarity. - Added unit tests for the `FounderSubdomainList` to ensure proper rendering and functionality. - Streamlined the `SubdomainManagerCard` by removing unnecessary founder-related logic and focusing on custom subdomain management.
…ionCard - Updated `getEnsRenewalQuoteAction` to include USD estimates based on the latest Ethereum spot price from Coinbase. - Enhanced `ExpiryExtensionCard` to display estimated total in USD and improved state management for renewal values. - Added new tests for the updated quote functionality and ensured proper rendering of the renewal input. - Introduced a new `RenewalValueInput` component for better handling of manual overrides in renewal values. - Refactored existing tests to accommodate changes in the renewal quote structure and ensure comprehensive coverage.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
src/app/api/safe/propose/route.ts
Outdated
| const safeInfo = await getSafeInfo(parsed.safeAddress, parsed.chainId) | ||
| const isSafeOwner = safeInfo?.owners.some( | ||
| (owner) => owner.toLowerCase() === senderWallet | ||
| ) | ||
| if (!isSafeOwner) { | ||
| return NextResponse.json( | ||
| { | ||
| error: 'Only Safe owners can submit ENS proposals for this company.', | ||
| }, | ||
| { status: 403 } | ||
| ) | ||
| } |
There was a problem hiding this comment.
Null safeInfo produces a misleading 403
getSafeInfo can return null when the Safe Transaction Service API is unreachable or returns an error. If it does, safeInfo?.owners.some(...) evaluates to undefined, !undefined is true, and the route responds with a 403 "Only Safe owners can submit ENS proposals" — a message that implies the caller has been rejected due to authorization, when in reality the dependency timed out.
A legitimate Safe owner who hits this during a momentary Safe API outage will have no way to distinguish a real rejection from a service error. The getSafeInfo call should be guarded:
const safeInfo = await getSafeInfo(parsed.safeAddress, parsed.chainId)
if (!safeInfo) {
return NextResponse.json(
{ error: 'Could not verify Safe ownership — Safe API unavailable.' },
{ status: 503 }
)
}
const isSafeOwner = safeInfo.owners.some(
(owner) => owner.toLowerCase() === senderWallet
)| } catch (error) { | ||
| handleSafeProposalError(error, 'Failed to propose trait update', { | ||
| onApiUnavailable: (msg) => setErrorMessage(msg), | ||
| onError: (msg) => setErrorMessage(msg), | ||
| }) | ||
| } finally { | ||
| setBusyKey(null) | ||
| } |
There was a problem hiding this comment.
Missing safeApiUnavailable state leaves buttons enabled after a key-missing error
EnsTraitsCard and SubdomainManagerCard both track a safeApiUnavailable boolean and use it to disable proposal buttons once the Safe API key error has been observed. SubdomainProfileCard only forwards the API-key-missing message into errorMessage via onApiUnavailable, but never disables the "Propose" or "Set primary name" buttons. This means a user on a deployment without SAFE_API_KEY can keep clicking action buttons, re-triggering the same error each time.
Align with the other cards by adding:
const [safeApiUnavailable, setSafeApiUnavailable] = useState(false)and updating onApiUnavailable to setSafeApiUnavailable(true), then incorporating safeApiUnavailable into the disabled condition.
- Added new tests for the `getEnsRenewalQuoteAction` to ensure accurate quote retrieval and handling. - Updated `ExpiryExtensionCard` tests to mock the new `getEnsRenewalQuoteAction` functionality and verify proper rendering. - Refactored `startupchain-client` to improve error handling and added tests for client initialization without required environment variables. - Enhanced existing tests in `actions.test.ts` to cover new wallet and public client functionalities. - Introduced a new test file for `startupchain-client` to validate client behavior under various configurations.
- Added `SafeProposalServiceNotice` component to display service unavailability messages across multiple components. - Replaced inline error messages in `EnsTraitsCard`, `ExpiryExtensionCard`, `FounderSubdomainList`, `SubdomainManagerCard`, and `SubdomainProfileCard` with the new component for consistency. - Updated state management to handle API availability checks and display appropriate notices when the Safe proposal service is unavailable. - Introduced tests for `SafeProposalServiceNotice` to ensure correct rendering and messaging.
|
@greptile |
… polling - Replaced inline error handling in `EnsTraitsCard`, `ExpiryExtensionCard`, `FounderSubdomainList`, `SubdomainManagerCard`, and `SubdomainProfileCard` with a centralized error management approach using `useSafeProposalError`. - Introduced `usePollingRefresh` hook to streamline polling logic for refreshing the router based on pending operations. - Added `WalletConnectionWarning` component to enhance user feedback regarding wallet connection requirements. - Created new hooks for better state management and error handling across components.
…orm values on subdomain selection - Updated `FounderSubdomainList` to convert subdomain labels to lowercase before setting them in the pending batch. - Added `useEffect` in `SubdomainProfileCard` to initialize form values with empty strings for all ENS trait keys when a new subdomain is selected.
|
@greptile |
|
@greptile update confidence score and description |
Enhances the ENS management dashboard with comprehensive subdomain controls and a quoted renewal system.
ENS Management & Subdomains
Renewal System
Security & Infrastructure
Relates to #43, #44
Greptile Summary
This PR significantly expands the ENS management dashboard with tabbed navigation, batch subdomain assignment for founders, a subdomain profile manager, and a real-time ENS renewal quote engine backed on-chain
rentPricereads and optional Coinbase spot pricing. The Safe proposal API route is hardened with session-wallet / sender matching and Safe ownership verification viagetSafeInfoForVerification, which properly distinguishes auth failures from service unavailability. Shared infrastructure is well-extracted:useSafeWalletconsolidates wallet-resolution logic,useSafeProposalErrorcentralises error routing, andproposeBatchSafeTransactionFromWalletsupports the new batch-subdomain flow.Three issues remain:
parseRenewalValue("0")returns0n(notnull), so validation guards in bothcanSubmitRenewalProposalandhandleProposeRenewalpermit 0-wei transactions that will revert on-chain. Add explicit=== 0nchecks.errorCallbacksclosure —handleErrormemoises an unstableerrorCallbacksobject (recreated each render) with empty dependencies. The pattern works because enclosed setState functions are stable, but is acknowledged by eslint-disable as an anti-pattern. Stabilise by memoising callbacks withuseCallbackanduseMemo.PublicClientinstances each time.Confidence Score: 3/5
errorCallbacksclosure inuseSafeProposalErroris acknowledged by eslint-disable and could silently break if future changes capture non-stable values; (3) thegetPublicClientcache bypass for unsupported chains is a minor inefficiency creating new clients on every call. All three are fixable with straightforward changes.Comments Outside Diff (12)
src/app/(app)/dashboard/ens/components/expiry-extension-card.tsx, line 1387-1393 (link)Zero-wei renewal proposal is allowed through
canSubmitRenewalProposalonly checksparseRenewalValue(valueState.renewalValue) !== null. However,parseRenewalValue('0')returns0n, which passes the null-check. This means a user (or a quote that legitimately returns 0 for some reason) can submit a renewal Safe transaction withvalue: "0", which the ENS controller will revert on-chain because the payablerenewfunction requiresmsg.value >= price.base + price.premium.The guard should also reject zero:
src/app/(app)/dashboard/ens/components/management-tabs.tsx, line 1488-1503 (link)All tab panels mount simultaneously, triggering side-effects for inactive tabs
The current implementation renders all four
motion.divpanels at once and hides inactive ones via thehiddenclass. React still mounts hidden components, so theiruseEffecthooks fire immediately on page load.ExpiryExtensionCardhas an effect guarded only bycanQuote && chainId— both of which are truthy from the first render whenchainIdandcontrollerAddressare provided — so the renewal-quote server action is invoked on every page load regardless of whether the user ever switches to the Renewal tab.To defer side-effects, render only the active tab's content rather than mounting all panels at once. You can keep the fade animation by wrapping the active content in a
motion.divkeyed onactiveTabwithinitialandanimateopacity props.src/app/(app)/dashboard/ens/components/founder-subdomain-list.tsx, line 2277-2281 (link)Pending batch never auto-clears for mixed-case label inputs
filledEntries[i].labelis trimmed but not lowercased before being stored inpendingBatch.labels. The on-chain subdomain name, however, is always normalized to lowercase bynormalizeSubdomainLabel(called insidebuildCreateSubdomainTransaction). The clearance effect compares these directly:If a user types
"Alice",pendingBatch.labels = ["Alice"]butsub.name === "alice"on-chain, so the strict-equality check always fails and the pending banner never disappears, even after the Safe transaction executes.The
filledEntriesmemo already has access to the raw value — it just needs a.toLowerCase()before it is stored:src/app/(app)/dashboard/ens/components/subdomain-profile-card.tsx, line 3621-3630 (link)Form values not reset when switching subdomains
formValues(the ENS trait input fields) are initialized once with empty strings and never reset whenselectedSubdomainchanges. If a user fills in some trait values foralice.acme.eth, then switches the selector tobob.acme.eth, the previously entered values persist. Clicking "Propose" will then propose those stale values againstbob.acme.eth— the full subdomain name the transaction targets is derived fromselectedSubdomain, not from which subdomain the user was editing.Add a
useEffectthat resetsformValueswhenever the selected subdomain changes:src/app/(app)/dashboard/ens/components/management-tabs.tsx, line 2530-2545 (link)All tab panels eagerly mounted, firing side-effects on hidden tabs
Every panel is currently rendered to the DOM on initial page load and hidden only via CSS. Because the components are mounted (not unmounted), all of their
useEffects execute immediately:ExpiryExtensionCardcallsgetEnsRenewalQuoteAction(a server action that hits the ENS controller and the Coinbase spot-price API) as soon as the page loads, even if the user never opens the "Renewal" tab.router.refresh()polling intervals fromEnsTraitsCardandSubdomainManagerCardwill both be active, doubling the number of server round-trips.Conditionally rendering only the active panel's content (so inactive components are truly unmounted) would prevent these unintended effects. If the
motion.divfade animation must be preserved, wrapping inAnimatePresencewith conditional content rendering achieves both.src/app/(app)/dashboard/ens/components/expiry-extension-model.ts, line 143-152 (link)Zero-value renewal proposal is allowed
parseRenewalValue("0")returns0n, which is notnull, socanSubmitRenewalProposalreturnstruefor a zero-value input whenever all other conditions are met. A Safe proposal submitted withvalue: "0"will succeed in the queue but revert on-chain because the ENS controller'srenewfunction ispayableand enforces thatmsg.value >= rentPrice.base + rentPrice.premium.This is reachable today: in the manual-override path (quote error state + override enabled), the user can type
0into the input and the Propose button will be enabled.src/app/(app)/dashboard/ens/components/expiry-extension-card.tsx, line 1370-1436 (link)Server action called inside
useEffectviolates "do not fetch in components"AGENTS.md explicitly states "do not fetch in components." Calling
getEnsRenewalQuoteActioninside auseEffectis client-side data fetching, regardless of the implementation being a server action.The project already lists React Query as the mechanism for "Async data fetching (chainId in keys)" (
system-flow.md). Using a query here would:latestQuoteRequestref (race-condition guard) and thevoid promise.then().catch()patternConsider extracting the quote fetch into a
useQuerycall keyed on[ensName, chainId, duration]instead of managing the request lifecycle manually.Rule Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
src/app/api/safe/propose/route.ts, line 16-22 (link)safeAddressandsenderAddressnot validated as Ethereum addressesBoth fields are validated only with
.min(1). A caller supplying a syntactically invalid address (e.g."not-an-address") will pass schema validation and reachgetSafeInfoForVerification, which will returnstatus: 'unavailable'(a 404 from the Safe API), yielding a misleading 503 rather than a 400. Add a regex or viem'sisAddresscheck via a Zod refine:src/app/(app)/dashboard/ens/components/expiry-extension-model.ts, line 143-146 (link)parseRenewalValue("0")returns0n(notnull), so this guard permits zero-wei transactions. The ENS controller'srenewfunction requiresmsg.value >= rentPriceand will revert on-chain, producing a Safe proposal that always fails at execution time.Add an explicit zero check:
The same issue exists in
expiry-extension-card.tsxat line 232 — fix the guard there as well.src/app/(app)/dashboard/ens/components/expiry-extension-card.tsx, line 231-235 (link)parseRenewalValuereturns0nfor input "0", notnull, so this guard allows zero-wei proposals. Add an explicit zero check:src/hooks/use-safe-proposal-error.ts, line 33-48 (link)errorCallbacksis a plain object literal recreated every render, buthandleErroris memoized with an empty dependency array. This closes over the first render'serrorCallbacks, creating a fragile pattern. The eslint-disable acknowledges the issue.Stabilise by wrapping callbacks with
useCallbackand memoisingerrorCallbacks:src/lib/blockchain/startupchain-client.ts, line 146-168 (link)The cache hit-check at line 149 uses
chainKey(the requested key), but the storage at line 166 useschainConfigKey(the default key for unsupported chains). Unsupported chain IDs bypass the cache: each call creates a freshPublicClientinstead of reusing the previous one.Cache under both keys so fallback calls are memoised:
Last reviewed commit: 608f743
Context used: