Home

Awesome

The ENS Manager App (V3)

Quick start

Install pnpm, then:

pnpm install
pnpm dev

Navigate to localhost:3000

Why does this app exist?

The purpose of the manager app is to expose the functionality of the ENS protocol in a user friendly manor.

Brief intro to ENS

ENS is a decentralized naming system that runs on the ethereum blockchain. The main purpose of ENS is to convert unfriendly blockchain addresses into human readable names (e.g. 0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5 -> nick.eth), but ENS has grown into so much more than that. For more info please visit our docs site.

Coming from web2

Web3 is the term used to describe the blockchain enabled web, web2 is the 'legacy' more centralized web. Much of this app is built in tech that will be familiar to many web2 devs.

<b>Web2 tech</b>

<b>Web3 tech</b>

Explaining the web3 parts

TBC

Setting up the development environment

You must have Docker installed to run the test environment. For more information on the environment, see ens-test-env.

Once installed, you can run:

pnpm denv
pnpm dev:glocal

You can now navigate to http://localhost:3000 to see the app running off of a local blockchain.

You will need a browser wallet to develop and test blockchain interactions. Download the Metamask browser extension here.

During the setup flow choose "Import existing wallet".

When it asks for your "Secret recovery phrase", use test test test test test test test test test test test junk

You can then follow the instructions here to connect metamask to our local dev blockchain.

You should now have 10,000 ETH (local network eth) in your wallet. You're a legend on your local machine.

Architecture

High level overview

High level architecture diagram

CI/CD

Github action scripts can be found in .github/workflows

The Manager App

Hosting

The App is hosted on cloudflare and IPFS

Major dependencies

Ens.js

Much of the logic around interacting with the ENS contracts has been extraced into this library. This is mostly so that we can help to make the experience of interacting with ENS as simple as possible for other developers.

Thorin

As we have many different applications, and also would like to support the community, we have developed a design system in order to ensure consistent styling across the board.

Application Architecture: Key files and concepts

Pages and components

Pages folder has basic route layout and basic react needed for rendering pages. These files should be kept relatively simple

Components that pages consume are kept in the components folder. This folder has a structure that mimics the structure of the pages folder. If a component is only used on a specific page then it goes into the corresponding folder in the components folder.

If a component is used across multiple pages and other components, then it goes into the atoms and molecules folder (link to atoms and molecules thingy).

useQuery

TBC

Transactions

TransactionStore.ts

Transaction store is responsible for keeping track of the state of transactions.

TransactionFlowProvider.ts

We noticed transactions always follow a similar pattern and so created an internal API to streamline this. A transaction flow is a series of steps that occur in a modal, culminating in a successful or failed transaction. Transactions can have either an intro or an input step before the transaction step.

Example of transaction with an input and multiple steps

Sync Provider

This is for when the graph is behind and we are waiting for it to catchup.

Notification system

TBC

Metadata service

ENS names are NFTs. NFTs can have metadata associated with them, that is data associated with them that is not stored directly on-chain. One of the main use cases of this is to display a nice image that represents the ENS name. You can see an example of this here.

https://metadata.ens.domains/

Cloudflare workers

avatar-upload: url, src

gas-estimate-worker: url, src

app-v3-maintenance: url

moonpay-worker: url, src

etherscan-api worker: url

Data indexing

The graph hosted service: url, src

Unit Test

pnpm test
pnpm test:watch
pnpm test:coverage

If you need to deploy a new subgraph

You shouldn't deploy the subgraph on top of the existing dataset, instead you should create a clean dataset (explained below).

  1. Start the test environment
pnpm denv --save
  1. Deploy the subgraph

After the deploy scripts have run, you can deploy the subgraph. Assuming you are in the ens-subgraph repo, you can use:

yarn setup
  1. Wait for the subgraph to sync

Similar to the update process, a good indicator of sync status is if you see this message:

no chain head update for 30 seconds, polling for update, component: BlockStream

Dissimilar to the update process however is that you will never need to mine blocks manually.

  1. Exit the test environment

You can exit out of the test environment using Ctrl+C.

Once exited, you can commit the data to your branch. You do not need to run a separate save command.

E2E Testing

Stateless vs Stateful

Our e2e tests are split into two categories, stateless and stateful. Stateless test use the development environment, are faster, and is the general recommended way to write integration tests. Occasionally, you may need to test a feature that requires an external API or service. In this case, you can use the stateful tests. These tests are slower,

Running the tests

Running the entire stateless test suite:

pnpm denv
pnpm dev:glocal
pnpm e2e

Running a single test within a browser:

pnpm denv
pnpm dev:glocal
pnpm e2e < filename >:< linenumber > --headed

Running the entire stateful test suite:

pnpm dev
pnpm e2e:stateful

Running a single stateful test within a browser:

pnpm dev
pnpm e2e:stateful < filename >:< linenumber > --headed

makeName

The most important function in the e2e stateless tests is makeName. This function is used to create a unique name for each test. This is important because we want to avoid any conflicts between tests.

Syntax
  const name = await makeName({
    label: 'name',
    type: 'legacy',
    owner: '0x1234567890123456789012345678901234567890',
    manager: '0x1234567890123456789012345678901234567890',
    resolver: '0x1234567890123456789012345678901234567890',
    records: {
      texts: [{
        key: 'text',
        value: 'value'
      }],
      coins: [{
        coin: 'eth',
        value: '0x1234567890123456789012345678901234567890'
      }],
      contentHash: 'bafybeico3uuyj3vphxpvbowchdwjlrlrh62awxscrnii7w7flu5z6fk77y',
      abi: await encodeAbi({ encodeAs: 'cbor', data: { test2: 'test2' } }),
    }
    subnames: [{
      label: 'subname',
      type: 'wrapped',
      owner: '0x1234567890123456789012345678901234567890',
      duration: 365,
      resolver: '0x1234567890123456789012345678901234567890',
      records: [{
        key: 'text',
        value: 'value'
      }]
    }]
  })
Parameters
nameOrNames

A single or an array of names to create. Each name can have the following properties:

label: string

The label of the name.

type: legacy | legacy-register | wrapped*

The type of the name. legacy names adopt the original data structure of ENS and are not ERC1155 complaint. wrapped names are names that have been wrapped with the NameWrapper contract and are ERC1155 compliant. legacy-register names simulate how mass registration services register names, usually without a resolver of other options that may increase gas.

owner: user | user2 | user3

defaults to owner

The address of the owner of the name.

manager: user | user2 | user3

defaults to value of owner

The address of the manager of the name. Only applicable to legacy and legacy-registr names.

duration: number

defaults to 365 days in seconds

The number of seconds the name will be registered for. Negative values are allowed to simulate names that have expired or are in the grace period.

secret: hex

defaults to a zero hex

The secret used during the register process. You will most likely not need to set this value.

resolver: address

defaults to the legacy resolver for legacy names and the latest resolver for wrapped names

The address of the resolver for the name. Used to test cases where the resolver is misconfgured or not set.

addr: address

defaults to the address of the owner

The address record for the name. Is used to test cases where the eth address is not set.

records: RecordOptions

The records for the name. Below is a type definition for the records object.

type RecordOptions = {
  texts: {
    key: string,
    value: string
  }[]
  coins: {
    coin: string | number,
    value: string
  }[]
  contentHash: string
  abi: AbiObject
}

fuses: FusesType

applicable to wrapped names only

The fuses to burn for a wrapped name. Below is a type definition for the fuses object. Note that PARENT_CANNOT_CONTROL is not fuse option as it is burned by default when a 2LD name is wrapped.

type FusesType = {
  named: Array<"CANNOT_UNWRAP" | "CANNOT_BURN_FUSES" | "CANNOT_TRANSFER" | "CANNOT_SET_RESOLVER" | "CANNOT_SET_TTL" | "CANNOT_CREATE_SUBDOMAIN" | "CANNOT_APPROVE">
}

subnames: SubnameType[]

The subnames for the name. Below is a type definition for the subname object.

type SubnameType = {
  label: string
  type: 'legacy' | 'wrapped'
  resolver: string
  records: RecordOptions
  duration: number
  subnames: SubnameType[]
  fuses: FusesType // Only applicable to wrapped names
}
options

timeOffset: number

defaults to 0

The duration in seconds to move the blockchain forward after the name has been registered. In rare use cases, usually when you are testing a name with a negative duration, the blockchain may need to be moved forward after all the transactions before it will resolve correctly.

syncSubgraph: boolean

defaults to true

Whether to wait for the subgraph to sync before returning the name. It is useful to set this value to false when you are testing a feature that does not rely on the subgraph to speed up the tests.

Returns

Returns a string for the 2LD name that is made up of the label with a timestamp appended to it and .eth TLD. The appended timestamp ensures that each time that a name is generated that it is unique.

Building and Starting

pnpm build
pnpm start

# Or with the test environment running
pnpm build:glocal
pnpm buildandstart:glocal

Debugging

To debug a single test:

pnpm denv
pnpm dev:glocal
pnpm playwright test --project=stateless --ui stateless/extendNames

PR builds

Cloudflare will automatically build and deploy a test site when pushed to a new PR branch.

External Package Local Development

  1. Install yalc globally:
npm i -g yalc
  1. Run relevant update script within external repo, for example:
# Example publish script for ENSjs, be aware this may have changed.
pnpm publish:local:ensjs
  1. Run pnpm install within this repo:
pnpm install

If updating an existing yalc installation, you can add the --force flag.

Coding guidelines

Testing philosophy

Our testing philosophy is user-centric, meaning we want to write out tests so that they resemble the way a user would use our app as much as possible. We've borrowed this from the excellent testing-library.

A user generally clicks, types and swipes, and so most tests should include one of these actions. A user may also load a page in a specific state (by clicking, typing or swiping outside of the app) so sometimes we just want to check a page renders correctly. The vast majority of our tests will be of these kinds.

For deeper parts of the codebase that aren't directly related to a user interaction, such as utility functions, the user is the developer. So simply test the code in the way a developer would use it.

Knip Configuration Guide

1. Install Knip:

Install Knip as a development dependency in your project:

pnpm add -D knip

2. Add a knip script to your package.json:

Add a script to your package.json for easy access to Knip:

{
  "scripts": {
    ...,
    "knip": "knip",
    "knip:fix": "knip --fix --allow-remove-files"
  }
}

3. Create Knip Configuration File:

Create a knip.config.ts file at the root of your project. For more detail of configuration options, refer to the knip.config.ts file in the ENSDomains repository.

4. Run Knip:

To analyze your project, run Knip using the following command:

pnpm knip

Knip will exit with code 1 if any issues are found, such as unused files, dependencies, or exports that need to be removed.

5. Review and Remove Unused Files

After Knip completes its analysis, review the results. Manually remove any unused files that are safe to delete, or let Knip handle it automatically with the following command:

pnpm knip:fix

Ensure you carefully examine any files marked for removal to avoid accidentally deleting necessary code.

6. Run Unit Tests and E2E Tests:

After removing files, it's important to run your unit and end-to-end tests to ensure that everything is still functioning correctly:

pnpm test:coverage
pnpm e2e