Skip to content

Refactor/ Domains controller and implement gwei names#2532

Open
PetromirDev wants to merge 5 commits into
v2from
refactor-domains-and-add-gwei-names
Open

Refactor/ Domains controller and implement gwei names#2532
PetromirDev wants to merge 5 commits into
v2from
refactor-domains-and-add-gwei-names

Conversation

@PetromirDev

@PetromirDev PetromirDev commented Jul 9, 2026

Copy link
Copy Markdown
Member

Reworks domain resolution so the wallet can resolve names from more than just ENS. Instead of ENS/Namoshi being hardcoded, each name service is now a small self-contained resolver, and adding a new ENS-compatible service is a few lines. Also adds GNS (.gwei) on top of this.

Changes:

  • Introduced a pluggable resolver layer - ENS-compatible services (ENS, Namoshi, GNS) share one resolution path and only differ by their universal resolver, chain and TLD
  • Added GNS (.gwei) resolution
  • Added GNS verification with Colibri
  • Reworked the domains cache to store resolved names, avatar and expiry per service rather than ENS-specific fields
  • Fixed: Forward resolution was being cached (introduced in 9bc4f88)

@PetromirDev

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🔒 No security concerns identified
📝 TODO sections

⚡ Recommended focus areas for review

Hardcoded resolver priority

getPrimaryName always iterates the global DEFAULT_RESOLVERS array to determine which service owns the primary name. However, DomainsController accepts a custom resolvers array in its constructor (intended for tests). When a non-default set or order is supplied, the controller still uses the hardcoded priority to decide which name is primary, leading to inconsistent avatar/expiry caching and wrong domainToAddresses entries.

export const getPrimaryName = (
  names: ResolvedNames | undefined
): { id: NameServiceId; name: string } | null => {
  if (!names) return null

  for (const resolver of DEFAULT_RESOLVERS) {
    const name = names[resolver.id]
    if (name) return { id: resolver.id, name }
  }

  return null
}
Null expiry incorrectly dropped

In #saveResolvedDomain, the non-primary branch sets expiry to existing?.expiry ?? undefined. Because the nullish-coalescing operator treats null as missing, an explicitly cached null expiry (meaning "fetched, but no expiry exists for this name") is overwritten with undefined. This erases the cached state and causes unnecessary expiry refetches on subsequent lookups.

// starting a duplicate, so the caller resolves only once the data is ready.

@PetromirDev

Copy link
Copy Markdown
Member Author

/review

@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🔒 No security concerns identified
📝 TODO sections

⚡ Recommended focus areas for review

Hardcoded resolver priority

carryOverExpiry and #saveResolvedDomain call getPrimaryName, which hardcodes the global DEFAULT_RESOLVERS order. DomainsController accepts a custom resolvers array (documented as overridable for tests), so when the array order differs from the global default, the primary name is computed against the wrong priority list. This causes incorrect isPrimary checks, stale expiry carryover, and wrong avatar assignment.

const carryOverExpiry = (existing: Domains[string] | undefined, nextNames: ResolvedNames) => {
  const previous = getPrimaryName(existing?.names ?? {})
  const next = getPrimaryName(nextNames)

  return previous?.id === next?.id && previous?.name === next?.name ? existing?.expiry : undefined
}
Silent success on missing provider

resolveDomain no longer verifies that the RPC provider is available before resolving. When EnsCompatibleResolver.resolve returns null because the provider is missing, the controller still sets resolveDomainsStatus[domain] = 'RESOLVED' and clears errors. Previously a missing provider emitted a user-facing error or set 'FAILED'; now the UI silently shows no result with no feedback that resolution failed due to an unavailable RPC.

await resolver
  .resolve(domain, this.#context())
  .then(async (result) => {
    if (result?.address) {
      // Verify before caching, so a mismatch throws into the catch and nothing bad is persisted.
      const isVerified = await this.#verifyResolvedAddress(resolver, domain, result.address)
      if (isVerified) this.verifiedDomainsStatus[domain] = 'VERIFIED'

      this.domainToAddresses[domain] = {
        address: getAddress(result.address),
        type: resolver.id
      }
      this.#saveResolvedDomain({
        address: result.address,
        avatar: result.avatar,
        expiry: result.expiry,
        domain,
        type: resolver.id
      })
    }
    this.resolveDomainsStatus[domain] = 'RESOLVED'
    delete this.resolveDomainsErrors[domain]
    await this.forceEmitUpdate()
    this.resolveDomainsStatus[domain] = undefined

@gergana95 gergana95 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested and works great, did not find any issues. Great work 🙌

}

matches(domain: string): boolean {
return domain.endsWith('.gwei')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This resolves donnoh.gwei but not DONNOH.GWEI. This may be correct, just checkin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants