UNPKG

@authereum/resolution

Version:
581 lines (526 loc) 18.4 kB
import BN from 'bn.js'; import Ens from './Ens'; import Zns from './Zns'; import Cns from './Cns'; import UdApi from './UdApi'; import { Blockchain, UnclaimedDomainResponse, ResolutionResponse, DefaultAPI, API, NamingServiceName, Web3Version0Provider, Web3Version1Provider, Provider, NamingServiceSource, SourceDefinition, NamehashOptions, NamehashOptionsDefault, DnsRecordType, DnsRecord, CryptoRecords, } from './publicTypes'; import { nodeHash } from './types'; import { EthersProvider } from './publicTypes'; import ResolutionError, { ResolutionErrorCode } from './errors/resolutionError'; import NamingService from './NamingService'; import { signedInfuraLink } from './utils'; import { Eip1993Factories } from './utils/Eip1993Factories'; import DnsUtils from './DnsUtils'; /** * Blockchain domain Resolution library - Resolution. * @example * ``` * import Resolution from '@unstoppabledomains/resolution'; * * let resolution = new Resolution({ blockchain: { * ens: { * url: "https://mainnet.infura.io/v3/12351245223", * network: "mainnet" * } * } * }); * * let domain = "brad.zil"; * resolution.addr(domain, "eth").then(addr => console.log(addr));; * ``` */ export default class Resolution { /** @internal */ readonly blockchain: boolean; /** @internal */ readonly ens?: Ens; /** @internal */ readonly zns?: Zns; /** @internal */ readonly cns?: Cns; /** @internal */ readonly api?: UdApi; constructor({ blockchain = true, api = DefaultAPI, }: { blockchain?: Blockchain | boolean; api?: API } = {}) { this.blockchain = !!blockchain; if (blockchain) { if (blockchain === true) { blockchain = {}; } const web3provider = blockchain.web3Provider; if (web3provider) { console.warn( 'Usage of `web3Provider` option is deprecated. Use `provider` option instead for each individual blockchain', ); } const ens = this.normalizeSource(blockchain.ens, web3provider); const zns = this.normalizeSource(blockchain.zns); const cns = this.normalizeSource(blockchain.cns, web3provider); if (ens) { this.ens = new Ens(ens); } if (zns) { this.zns = new Zns(zns); } if (cns) { this.cns = new Cns(cns); } } else { this.api = new UdApi(api); } } /** * Creates a resolution with configured infura id for ens and cns * @param infura infura project id * @param network ethereum network name */ static infura(infura: string, network = 'mainnet'): Resolution { return new this({ blockchain: { ens: { url: signedInfuraLink(infura, network), network }, cns: { url: signedInfuraLink(infura, network), network }, }, }); } /** * Creates a resolution instance with configured provider * @param provider - any provider compatible with EIP-1193 * @see https://eips.ethereum.org/EIPS/eip-1193 */ static fromEip1193Provider(provider: Provider): Resolution { return new this({ blockchain: { zns: true, ens: { provider }, cns: { provider } }, }); } /** * Create a resolution instance from web3 0.x version provider * @param provider - an 0.x version provider from web3 ( must implement sendAsync(payload, callback) ) * @see https://github.com/ethereum/web3.js/blob/0.20.7/lib/web3/httpprovider.js#L116 */ static fromWeb3Version0Provider(provider: Web3Version0Provider): Resolution { return this.fromEip1193Provider( Eip1993Factories.fromWeb3Version0Provider(provider), ); } /** * Create a resolution instance from web3 1.x version provider * @param provider - an 1.x version provider from web3 ( must implement send(payload, callback) ) * @see https://github.com/ethereum/web3.js/blob/1.x/packages/web3-core-helpers/types/index.d.ts#L165 * @see https://github.com/ethereum/web3.js/blob/1.x/packages/web3-providers-http/src/index.js#L95 */ static fromWeb3Version1Provider(provider: Web3Version1Provider): Resolution { return this.fromEip1193Provider( Eip1993Factories.fromWeb3Version1Provider(provider), ); } /** * Creates instance of resolution from provider that implements Ethers Provider#call interface. * This wrapper support only `eth_call` method for now, which is enough for all the current Resolution functionality * @param provider - provider object * @see https://github.com/ethers-io/ethers.js/blob/v4-legacy/providers/abstract-provider.d.ts#L91 * @see https://github.com/ethers-io/ethers.js/blob/v5.0.4/packages/abstract-provider/src.ts/index.ts#L224 * @see https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#jsonrpcprovider-inherits-from-provider * @see https://github.com/ethers-io/ethers.js/blob/master/packages/providers/src.ts/json-rpc-provider.ts */ static fromEthersProvider(provider: EthersProvider): Resolution { return this.fromEip1193Provider( Eip1993Factories.fromEthersProvider(provider), ); } /** * Resolves the given domain * @async * @param domain - domain name to be resolved * @returns A promise that resolves in an object */ async resolve(domain: string): Promise<ResolutionResponse> { domain = this.prepareDomain(domain); const method = this.getNamingMethodOrThrow(domain); const result = await method.resolve(domain); return result || UnclaimedDomainResponse; } /** * Resolves given domain name to a specific currency address if exists * @async * @param domain - domain name to be resolved * @param currencyTicker - currency ticker like BTC, ETH, ZIL * @deprecated since Resolution v1.7.0 * @returns A promise that resolves in an address or null */ async address( domain: string, currencyTicker: string, ): Promise<string | null> { console.warn( 'Resolution#address is deprecated since v1.7.0, use Resolution#addr instead', ); domain = this.prepareDomain(domain); try { return await this.addressOrThrow(domain, currencyTicker); } catch (error) { if (error instanceof ResolutionError) { return null; } else { throw error; } } } /** * Resolves given domain name to a specific currency address if exists * @async * @param domain - domain name to be resolved * @param currencyTicker - currency ticker like BTC, ETH, ZIL * @throws [[ResolutionError]] if address is not found * @returns A promise that resolves in an address */ async addr(domain: string, currrencyTicker: string): Promise<string> { return await this.record( domain, `crypto.${currrencyTicker.toUpperCase()}.address`, ); } /** * Resolves given domain name to a verified twitter handle * @async * @param domain - domain name to be resolved * @throws [[ResolutionError]] if twitter is not found * @returns A promise that resolves in a verified twitter handle */ async twitter(domain: string): Promise<string> { domain = this.prepareDomain(domain); const namingService = this.serviceName(domain); if (namingService !== 'CNS') { throw new ResolutionError(ResolutionErrorCode.UnsupportedMethod, { domain, methodName: 'twitter', }); } const method = this.getNamingMethodOrThrow(domain); return method.twitter(domain); } /** * Resolve a chat id from the domain record * @param domain - domain name to be resolved * @throws [[ResolutionError]] * @returns A promise that resolves in chatId */ async chatId(domain: string): Promise<string> { return await this.record(domain, 'gundb.username.value'); } /** * Resolve a gundb public key from the domain record * @param domain - domain name to be resolved * @throws [[ResolutionError]] * @returns a promise that resolves in gundb public key */ async chatPk(domain: string): Promise<string> { return await this.record(domain, 'gundb.public_key.value'); } /** * Resolves the IPFS hash configured for domain records on ZNS * @param domain - domain name * @throws [[ResolutionError]] */ async ipfsHash(domain: string): Promise<string> { domain = this.prepareDomain(domain); return await this.getPreferableNewRecord(domain, 'dweb.ipfs.hash', 'ipfs.html.value'); } /** * Resolves the httpUrl attached to domain * @param domain - domain name */ async httpUrl(domain: string): Promise<string> { domain = this.prepareDomain(domain); return await this.getPreferableNewRecord(domain, 'browser.redirect_url', 'ipfs.redirect_domain.value'); } /** * Resolves the ipfs redirect url for a supported domain records * @deprecated since v1.0.15 use Resolution#httpUrl instead * @param domain - domain name * @throws [[ResolutionError]] * @returns A Promise that resolves in redirect url */ async ipfsRedirect(domain: string): Promise<string> { console.warn( 'Resolution#ipfsRedirect is deprecated since v1.0.15, use Resolution#httpUrl instead', ); return await this.record(domain, 'ipfs.redirect_domain.value'); } /** * Resolves the ipfs email field from whois configurations * @param domain - domain name * @throws [[ResolutionError]] * @returns A Promise that resolves in an email address configured for this domain whois */ async email(domain: string): Promise<string> { return await this.record(domain, 'whois.email.value'); } /** * @returns A specific currency address or throws an error * @param domain domain name * @param currencyTicker currency ticker such as * - ZIL * - BTC * - ETH * @throws [[ResolutionError]] if address is not found * @deprecated since v1.7.0 use Resolution#addr instead */ async addressOrThrow( domain: string, currencyTicker: string, ): Promise<string> { console.warn( 'Resolution#addressOrThrow is deprecated since v1.7.0, use Resolution#addr instead', ); domain = this.prepareDomain(domain); const method = this.getNamingMethodOrThrow(domain); try { const addr = await method.record( domain, `crypto.${currencyTicker.toUpperCase()}.address`, ); return addr; } catch (error) { // re-throw an error for back compatability. old method throws deprecated UnspecifiedCurrency code since before v1.7.0 if ( error instanceof ResolutionError && error.code === ResolutionErrorCode.RecordNotFound ) { throw new ResolutionError(ResolutionErrorCode.UnspecifiedCurrency, { domain, currencyTicker, }); } throw error; } } /** * @returns the resolver address for a specific domain * @param domain - domain to look for */ async resolver(domain: string): Promise<string> { domain = this.prepareDomain(domain); const resolver = await this.getNamingMethodOrThrow(domain).resolver(domain); if (!resolver) { throw new ResolutionError(ResolutionErrorCode.UnspecifiedResolver, {domain}); } return resolver; } /** * @param domain - domain name * @returns An owner address of the domain */ async owner(domain: string): Promise<string | null> { domain = this.prepareDomain(domain); const method = this.getNamingMethodOrThrow(domain); return (await method.owner(domain)) || null; } /** * @param domain - domain name * @param recordKey - a name of a record to be resolved * @returns A record value promise for a given record name */ async record(domain: string, recordKey: string): Promise<string> { domain = this.prepareDomain(domain); const method = this.getNamingMethodOrThrow(domain); return await method.record(domain, recordKey); } /** * @param domain domain name * @param keys Array of record keys to be resolved * @returns A Promise with key-value mapping of domain records */ async records(domain: string, keys: string[]): Promise<CryptoRecords> { domain = this.prepareDomain(domain); const method = this.getNamingMethodOrThrow(domain); return await method.records(domain, keys); } /** * This method is only for ens at the moment. Reverse the ens address to a ens registered domain name * @async * @param address - address you wish to reverse * @param currencyTicker - currency ticker like BTC, ETH, ZIL * @returns Domain name attached to this address */ async reverse( address: string, currencyTicker: string, ): Promise<string | null> { return (this.findNamingService(NamingServiceName.ENS) as Ens).reverse( address, currencyTicker, ); } /** * @returns Produces a namehash from supported naming service in hex format with 0x prefix. * Corresponds to ERC721 token id in case of Ethereum based naming service like ENS or CNS. * @param domain domain name to be converted * @param options formatting options * @throws [[ResolutionError]] with UnsupportedDomain error code if domain extension is unknown */ namehash(domain: string, options: NamehashOptions = NamehashOptionsDefault): string { domain = this.prepareDomain(domain); return this.formatNamehash(this.getNamingMethodOrThrow(domain).namehash(domain), options); } /** * @returns a namehash of a subdomain with name label * @param parent namehash of a parent domain * @param label subdomain name * @param method "ENS", "CNS" or "ZNS" * @param options formatting options */ childhash( parent: nodeHash, label: string, method: NamingServiceName, options: NamehashOptions = NamehashOptionsDefault, ): nodeHash { return this.formatNamehash(this.findNamingService(method).childhash(parent, label), options); } private formatNamehash(hash, options: NamehashOptions) { hash = hash.replace('0x', ''); if (options.format === 'dec') { return new BN(hash, 'hex').toString(10); } else { return options.prefix ? '0x' + hash : hash; } } /** * Checks weather the domain name matches the hash * @param domain - domain name to check againt * @param hash - hash obtained from the blockchain */ isValidHash(domain: string, hash: string): boolean { domain = this.prepareDomain(domain); return this.namehash(domain) === hash; } /** * Checks if the domain name is valid according to naming service rules * for valid domain names. * Example: ENS doesn't allow domains that start from '-' symbol. * @param domain - domain name to be checked */ isSupportedDomain(domain: string): boolean { domain = this.prepareDomain(domain); return !!this.getNamingMethod(domain); } /** * Checks if the domain is supported by the specified network as well as if it is in valid format * @param domain - domain name to be checked */ isSupportedDomainInNetwork(domain: string): boolean { domain = this.prepareDomain(domain); const method = this.getNamingMethod(domain); return !!method && method.isSupportedNetwork(); } /** * Returns the name of the service for a domain ENS | CNS | ZNS * @param domain - domain name to look for */ serviceName(domain: string): NamingServiceName { domain = this.prepareDomain(domain); return this.getNamingMethodOrThrow(domain).serviceName(domain); } /** * Returns all record keys of the domain. * This method is strongly unrecommended for production use due to lack of support for many ethereum service providers and low performance * Method is not supported by ENS * @param domain - domain name */ async allRecords(domain: string): Promise<CryptoRecords> { domain = this.prepareDomain(domain); return await this.getNamingMethodOrThrow(domain).allRecords(domain); } async dns(domain: string, types: DnsRecordType[]): Promise<DnsRecord[]> { const dnsUtils = new DnsUtils(); domain = this.prepareDomain(domain); const method = this.getNamingMethodOrThrow(domain); const dnsRecordKeys = this.getDnsRecordKeys(types); const blockchainData = await method.records(domain, dnsRecordKeys); return dnsUtils.toList(blockchainData); } private getDnsRecordKeys(types: DnsRecordType[]): string[] { const records = ['dns.ttl']; types.forEach(type => { records.push(`dns.${type}`); records.push(`dns.${type}.ttl`); }); return records; } private async getPreferableNewRecord(domain: string, newRecord: string, oldRecord: string): Promise<string> { const records = await this.records(domain, [newRecord, oldRecord]) as Record<string, string>; return NamingService.ensureRecordPresence(domain, newRecord, records[newRecord] || records[oldRecord]); } private getNamingMethod(domain: string): NamingService | undefined { return this.getResolutionMethods().find(method => method.isSupportedDomain(domain), ); } private getResolutionMethods(): NamingService[] { return (this.blockchain ? ([this.ens, this.zns, this.cns] as NamingService[]) : ([this.api] as NamingService[]) ).filter(v => v); } private getNamingMethodOrThrow(domain: string): NamingService { const method = this.getNamingMethod(domain); if (!method) { throw new ResolutionError(ResolutionErrorCode.UnsupportedDomain, { domain, }); } return method; } private findNamingService(name: NamingServiceName): NamingService { const service = this.getResolutionMethods().find(m => m.name === name); if (!service) { throw new ResolutionError(ResolutionErrorCode.NamingServiceDown, { method: name, }); } return service; } private prepareDomain(domain: string): string { return domain ? domain.trim().toLowerCase() : ''; } private normalizeSource( source: NamingServiceSource | undefined, provider?: Provider, ): SourceDefinition | false { switch (typeof source) { case 'undefined': { return { provider }; } case 'boolean': { return source ? { provider } : false; } case 'string': { return { url: source }; } case 'object': { return { provider, ...source }; } } throw new Error('Unsupported configuration'); } } export { Resolution };