The ultimate ENS javascript library, with ethers.js under the hood.
NOTE!!!
ENSjs v3 is currently in the early development stage, meaning that the APIs are subject to change.
We also use undeployed contracts under the hood, so this will not work on any mainnet/testnet where the contracts are not deployed.
Given the current development status, we're actively seeking feedback so feel free to create an issue or PR if you notice something!
If you are looking for documentation for version 2, it an be found here.
Features
- Dynamically load everything
- Super fast response times (1 call for most RPC calls)
- Easy call batchability
- Written in TypeScript
- Supports the most cutting edge ENS features
-
Installation
Install @ensdomains/ensjs, alongside ethers.
npm install @ensdomains/ensjs ethers
Getting Started
All that's needed to get started is an ethers provider instance.
Once you create a new ENS instance, you can pass it in using setProvider.
import { ENS } from '@ensdomains/ensjs'
import { ethers } from 'ethers'
const provider = new ethers.providers.JsonRpcProvider(providerUrl)
const ENSInstance = new ENS()
await ENSInstance.setProvider(provider)
NOTE:
If using ENSjs with Node, you may need to pass the --experimental-specifier-resolution=node
flag.
node --experimental-specifier-resolution=node ./index.js
Batching Calls
The batch function is a large part of this library, and there are plenty of situations where you might want to use it.
Note that only functions with the GeneratedRawFunction
type can be batched together.
const batched = await ENSInstance.batch(
ENSInstance.getText.batch('test.eth', 'foo'),
ENSInstance.getAddr.batch('test.eth'),
ENSInstance.getOwner.batch('test.eth'),
)
Using Custom Graph Node URIs
If you want to use your own graph-node URI, such as a local graph-node URI, you can pass it through when creating a new ENS instance.
Alternatively, if you don't want to use The Graph at all you can pass through null
.
import { ENS } from '@ensdomains/ensjs'
const ENSInstance = new ENS({
graphURI: 'http://localhost:8000/subgraphs/name/graphprotocol/ens',
})
const ENSInstance = new ENS({ graphURI: null })
Single-use Providers
If you want to use a specific provider to make a single call occasionally, you can easily do so.
import { ENS } from '@ensdomains/ensjs'
const ENSInstance = new ENS()
const callWithProvider = await ENSInstance.withProvider(otherProvider).getText(
'test.eth',
'foo',
)
Profiles
You can fetch almost all information about an ENS name (or address) using getProfile.
If an address is used as the first argument, it will fetch the primary name and give the same response as a name would.
It will automatically get all the records for a name, as well as get the resolver address for the name.
Specific records can also be used as an input, if you only want to get certain ones. If an address is used as an input alongside this,
you also save 1 RPC call.
NOTE:
The profile function will always request an ETH addr record.
For names, this means the address will always at the top level of the returned object.
For addresses, this means the "match" property (a boolean value for matching reverse/forward resolution) will always be at the top level of the returned object.
const profile = await ENSInstance.getProfile('test.eth')
const profile = await ENSInstance.getProfile(
'0xeefB13C7D42eFCc655E528dA6d6F7bBcf9A2251d',
)
const profile = await ENSInstance.getProfile('test.eth', {
texts: true,
coinTypes: true,
contentHash: true,
})
const profile = await ENSInstance.getProfile('test.eth', {
texts: ['foo'],
coinTypes: ['ETH'],
})
Returns:
type RecordItem = {
key: string | number
type: 'addr' | 'text' | 'contentHash'
coin?: string
addr?: string
value?: string
}
type ProfileReturn = {
address?: string
name?: string
records: {
contentHash?: ContentHashObject | null
texts?: RecordItem[]
coinTypes?: RecordItem[]
}
resolverAddress: string
}
Name History
Getting the history for a name is very simple and can be done in two ways.
Not all data can be immediately fetched for the history of an ENS name, which is why there is multiple methods for doing so.
Text records do not contain the string value of the changed record, only the key. The value needs to be derived from fetching
the individual transaction hash. This can potentially be very slow if the name has a long history.
const history = await ENSInstance.getHistory('test.eth')
const detail = await ENSInstance.getHistoryDetailForTransactionHash(
transactionHash,
optionalIndex,
)
const historyWithDetail = await ENSInstance.getHistoryWithDetail('test.eth')
Ownership Levels
The getOwner
function returns not only an owner (and potentially a registrant), but also a ownershipLevel value.
This value essentially means the contract for the "real owner" of any given name. In most cases it means the NFT contract
of the name, but if there is no NFT then it's just the registry. This value is useful for input into the transferName
function, where a contract needs to be specified.
Wrapping Names
Wrapping names is very simple, you can wrap any name from the same function, with the exact contract to use being inferred.
You can specify both the fuses and resolver address to use with the wrapped name, but it's entirely optional.
const tx = await ENSInstance.wrapName(
'test.eth',
'0xeefB13C7D42eFCc655E528dA6d6F7bBcf9A2251d',
)
const tx = await ENSInstance.wrapName(
'sub.test.eth',
'0xeefB13C7D42eFCc655E528dA6d6F7bBcf9A2251d',
)
Write Transaction Options
Currently, some write functions have an options
argument. While this may expand over time,
it currently just allows you to pass an address or index for an account array to ethers for specifying the signer of the transaction.
Internal Structure
Raw Functions
Raw functions are a crucial part of how ENSjs works. In the function file itself
a raw
and decode
function both need to be defined, with the export being an object with those properties.
This allows for the encoding and decoding of contract calls to be split, meaning that multiple calls can be batched together.
For calling a raw function by itself, the raw and decode functions are stitched together with a provider call. This is done
using importGenerator
which is explained below.
importGenerator
The importGenerator function generates a wrapped function for any given input.
The result of the wrapped function obfuscates the processing that ENSjs does, and exposes a cleaner API to the user/developer.
The reason we do this is to:
- Pass through all the required variables for the function
- Split individual functions from the main class
- Dynamically load functions and their dependencies
- Allow each function's dependencies to be imported regularly
- Remove duplicate code
- Make it easier to isolate errors
- Stitch
raw
and decode
functions together
ContractManager
The contract manager is where all the contracts are dynamically loaded in and resolved based on the network.
A new instance of ContractManager is created every time you switch providers.
GqlManager
The GQL manager is used as to separate the reliance of ENSjs from GQL.
It only loads in GQL when it is needed, or not at all if specified in the constructor of the ENS class.
Very simply, it just exposes the core functions needed for ENSjs which can then be accessed.
initialProvider
The initialProvider
, and similarly checkInitialProvider
are used when creating single-use class instances with withProvider
.
It allows withProvider
to act as a new ENS instance without having to await a promise, which simplifies the API.
checkInitialProvider
is run on every function call given that it's extremely lightweight.
Individual Functions
Utils
Utils can be imported at follows
import { encodeContenthash } from '@ensdomains/ensjs/utils/contentHash'
getFuses
Gets the fuses for a specified wrapped name.
Input:
Output:
fuseObj
: object
vulnerability
: string
- Vulnerability for name
- Will be "Safe" if no vulnerability
vulnerableNode
: string | null
- Node that is vulnerable in chain
rawFuses
: BigNumber
getHistory
Gets the history for a specified name.
Input:
Output:
domain
: array
registration
: array
resolver
: array
getHistoryWithDetail
Gets the history for a specified name with details.
Input:
Output:
domain
: array
registration
: array
resolver
: array
getHistoryDetailForTransactionHash
Gets the history details for a specified transaction hash.
Input:
hash
: string
indexInTransaction
: number?
- Index of transaction out of same transaction type
Output:
- object | array
key
: string
value
: string
getName
Gets the primary name for a specified address.
Input:
Output:
name
: string | null
match
: boolean
- Forward resolved match check value
getOwner
Gets the owner of a specified name. See ownership levels for more details about the output.
Input:
Output:
owner
: string
- Controller of records for name
registrant
: string?
ownershipLevel
: string
- Level at which the ownership data is being read
getProfile
Gets the profile of a specified name or address, or just certain records if specified.
Input:
nameOrAddress
: string
options
: object?
contentHash
: boolean?texts
: boolean? | string[]?
- Array of keys, or true for all keys
coinTypes
: boolean? | string[]?
- Array of keys, or true for all keys
Output:
resolverAddress
: string
records
: object
- matching records from input
contentHash
: object? | null?
texts
: array?
coinTypes
: array?
key
: numbercoin
: string
value
: string
name
: string?
- Only applicable for address inputs
- Resolved name
address
: string?
- Only applicable for name inputs
- Resolved address
match
: boolean?
- Only applicable for address inputs
- Forward resolved match check value
getResolver
Gets the resolver for a specified name.
Input:
Output:
getContentHash
Gets the content hash record for a specified name.
Input:
Output:
getText
Gets a text record for a specified name.
Input:
Output:
getAddr
Gets an address record for a specified name.
Input:
name
: string
coinType
: string? | number?
- Target coin
- Defaults to 60 (ETH) if undefined
Output:
burnFuses
Creates a transaction to burn fuses on a specified wrapped name.
Input:
name
: string
fusesToBurn
: object
- Object of fuses intended to be burned.
Output:
createSubname
Creates a subname using the specified contract.
Input:
- object
name
: string
owner
: string
contract
: registry
| nameWrapper
resolverAddress
: string?
- Resolver address for name
shouldWrap
: boolean?
- Only valid with NameWrapper contract
- Initial name wrapped state
fuses
: object?
- Only valid with NameWrapper contract
- Initial fuses to be burned
Output:
deleteSubname
Deletes a subname using the specified contract.
Input:
name
: string
contract
: registry
| nameWrapper
Output:
transferSubname
Transfers a subname using the specified contract.
Please note that transferring a wrapped name using this method will unwrap the name.
Input:
name
: string
contract
: registry
| nameWrapper
address
: string
- Address to transfer name to
Output:
setName
Sets the primary name for a specified address.
Input:
name
: string
address
: string?
- Setting other primary names requires authorisation
- Address to set name for
resolver
: string?
- Setting other primary names requires authorisation
- Target resolver
Output:
setRecords
Sets multiple records at once for the specified name.
Input:
name
: string
records
: object
contentHash
: string?
- Formatted and encoded content hash
texts
: array?
- object
key
: string
value
: string
coinTypes
: array?
- object
key
: string
value
: string
Output:
setResolver
Sets the resolver for the specified name, using the specified contract.
Input:
name
: string
contract
: registry
| nameWrapper
resolver
: string?
- Leaving this undefined will use the default public resolver
Output:
transferName
Transfers a name, using the specified contract.
Input:
name
: string
newOwner
: string
- Address to transfer name to
contract
: registry
| nameWrapper
| baseRegistrar
Output:
wrapName
Wraps a name.
Input:
name
: string
wrappedOwner
: string
- New owner of wrapped name
fuseOptions
: object?
resolverAddress
: string?
Output:
unwrapName
Unwraps a name.
Input:
name
: string
newController
: string
newRegistrant
: string?
Output:
universalWrapper
Wraps a function so it is directed to the universal resolver instead of the default public resolver.
Input:
name
: string
data
: string
- Hex encoded function data
Output:
- object
data
: string
- Hex encoded function result
resolver
: string
Examples for universalWrapper can be found in getSpecificRecord
.
resolverMulticallWrapper
Wraps multiple resolver calls so they are made into a single resolver multicall.
Input:
- array
- object
to
: string
- Placeholder for standard function calls, ignore this.
data
: string
- Hex encoded function data
Output:
- array
- string
- Hex encoded function result
Examples for resolverMulticallWrapper can be found in getProfile
.