ens-did-resolver
Version:
Resolve DID documents for ENS
204 lines (188 loc) • 6.68 kB
text/typescript
import { EnsResolver, Provider, Web3Provider } from '@ethersproject/providers'
import { DIDDocument, DIDResolutionResult, DIDResolver, ParsedDID, Service, VerificationMethod } from 'did-resolver'
import { ConfigurationOptions, configureResolverWithNetworks } from './configuration'
import { Errors, identifierMatcher, isDefined } from './helpers'
export function getResolver(config?: ConfigurationOptions): Record<string, DIDResolver> {
async function resolve(did: string, parsed: ParsedDID): Promise<DIDResolutionResult> {
const networks = configureResolverWithNetworks(config)
// check if identifier(parsed.id) contains a network code
const fullId = parsed.id.match(identifierMatcher)
if (!fullId) {
return {
didResolutionMetadata: {
error: Errors.invalidDid,
message: `Not a valid did:ens: ${parsed.id}`,
},
didDocumentMetadata: {},
didDocument: null,
}
}
const ensName = fullId[2]
const networkCode = typeof fullId[1] === 'string' ? fullId[1].slice(0, -1) : ''
// get provider for that network or the mainnet provider if none other is given
const provider: Provider = networks[networkCode]
if (!provider || typeof provider === 'undefined') {
return {
didResolutionMetadata: {
error: Errors.unknownNetwork,
message: `This resolver is not configured for the ${networkCode} network required by ${
parsed.id
}. Networks: ${JSON.stringify(Object.keys(networks))}`,
},
didDocumentMetadata: {},
didDocument: null,
}
}
const ensResolver: EnsResolver | null = await (provider as Web3Provider).getResolver(ensName)
if (!ensResolver) {
return {
didResolutionMetadata: {
error: Errors.unknownEnsResolver,
message: `This network (${networkCode}), required by ${parsed.id}, does not have a known ENS resolver`,
},
didDocumentMetadata: {},
didDocument: null,
}
}
let err: string | null = null
let address: string | null = null
try {
address = await ensResolver.getAddress()
} catch (error) {
err = `resolver_error: Cannot resolve ENS name: ${error}`
}
const didDocumentMetadata = {}
let didDocument: DIDDocument | null = null
if (address) {
const chainId = (await provider.getNetwork()).chainId
const blockchainAccountId = `${address}@eip155:${chainId}`
const postfix = address
// setup default did doc
didDocument = {
id: did,
service: [
{
id: `${did}#Web3PublicProfile-${postfix}`,
type: 'Web3PublicProfile',
serviceEndpoint: ensName,
},
],
verificationMethod: [
{
id: `${did}#${postfix}`,
type: 'EcdsaSecp256k1RecoveryMethod2020',
controller: did,
blockchainAccountId,
},
],
authentication: [`${did}#${postfix}`],
capabilityDelegation: [`${did}#${postfix}`],
capabilityInvocation: [`${did}#${postfix}`],
assertionMethod: [`${did}#${postfix}`],
}
}
const getEnsRecord = async <T>(ensResolver: EnsResolver, name: string): Promise<T | null> => {
let parsedEntry: T | null = null
const entry = await ensResolver.getText(name)
if (entry) {
try {
parsedEntry = JSON.parse(unescape(entry))
} catch (e) {
return null
}
}
return parsedEntry
}
const filterValidVerificationMethods = (
did: string,
current: (string | VerificationMethod)[],
all: VerificationMethod[]
): (string | VerificationMethod)[] => {
const methodLinks = (current.filter((entry) => typeof entry === 'string') as string[])
.map((entry) => (entry.startsWith('#') ? `${did}${entry}` : entry))
.filter((entry) => all?.some((b) => b.id === entry))
const fullMethods = (
current.filter(
(entry) =>
entry != null &&
typeof entry === 'object' &&
Object.keys(entry).includes('id') &&
Object.keys(entry).includes('type') &&
Object.keys(entry).some((k) => k.startsWith('publicKey'))
) as VerificationMethod[]
).map((entry: VerificationMethod) => {
entry.controller = entry.controller || did
if (entry.id.startsWith('#')) {
entry.id = `${did}${entry}`
}
return entry
})
return [...methodLinks, ...fullMethods]
}
const services = (await getEnsRecord<Service[]>(ensResolver, 'org.w3c.did.service')) || []
if (services) {
if (didDocument) {
didDocument.service = [...(didDocument.service || []), ...services].filter(isDefined)
}
}
const verificationMethods =
(await getEnsRecord<VerificationMethod[]>(ensResolver, 'org.w3c.did.verificationMethod')) || []
if (verificationMethods) {
verificationMethods.map((method) => {
if (method.id.startsWith('#')) {
method.id = `${did}${method.id}`
}
method.controller = method.controller || did
return method
})
if (didDocument) {
didDocument.verificationMethod = [...(didDocument.verificationMethod || []), ...verificationMethods].filter(
isDefined
)
}
}
const relationships = [
'keyAgreement',
'assertionMethod',
'authentication',
'capabilityInvocation',
'capabilityDelegation',
]
await relationships.reduce(async (memo, relationship) => {
await memo
try {
const verificationMethod =
(await getEnsRecord<(string | VerificationMethod)[]>(ensResolver, `org.w3c.did.${relationship}`)) || []
if (verificationMethod) {
if (didDocument) {
didDocument[relationship] = [
...(didDocument[relationship] || []),
...filterValidVerificationMethods(did, verificationMethod, verificationMethods),
]
}
}
} catch (e) {
// nop
}
}, Promise.resolve())
const contentType =
typeof didDocument?.['@context'] !== 'undefined' ? 'application/did+ld+json' : 'application/did+json'
if (err) {
return {
didDocument,
didDocumentMetadata,
didResolutionMetadata: {
error: Errors.notFound,
message: err,
},
}
} else {
return {
didDocument,
didDocumentMetadata,
didResolutionMetadata: { contentType },
}
}
}
return { ens: resolve }
}