ens-resolver.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. /**
  2. * ENS is a service which allows easy-to-remember names to map to
  3. * network addresses.
  4. *
  5. * @_section: api/providers/ens-resolver:ENS Resolver [about-ens-rsolver]
  6. */
  7. import { getAddress } from "../address/index.js";
  8. import { ZeroAddress } from "../constants/index.js";
  9. import { Contract } from "../contract/index.js";
  10. import { dnsEncode, namehash } from "../hash/index.js";
  11. import {
  12. hexlify, isHexString, toBeHex,
  13. defineProperties, encodeBase58,
  14. assert, assertArgument, isError,
  15. FetchRequest
  16. } from "../utils/index.js";
  17. import type { FunctionFragment } from "../abi/index.js";
  18. import type { BytesLike } from "../utils/index.js";
  19. import type { AbstractProvider, AbstractProviderPlugin } from "./abstract-provider.js";
  20. import type { EnsPlugin } from "./plugins-network.js";
  21. import type { Provider } from "./provider.js";
  22. // @TODO: This should use the fetch-data:ipfs gateway
  23. // Trim off the ipfs:// prefix and return the default gateway URL
  24. function getIpfsLink(link: string): string {
  25. if (link.match(/^ipfs:\/\/ipfs\//i)) {
  26. link = link.substring(12);
  27. } else if (link.match(/^ipfs:\/\//i)) {
  28. link = link.substring(7);
  29. } else {
  30. assertArgument(false, "unsupported IPFS format", "link", link);
  31. }
  32. return `https:/\/gateway.ipfs.io/ipfs/${ link }`;
  33. }
  34. /**
  35. * The type of data found during a steip during avatar resolution.
  36. */
  37. export type AvatarLinkageType = "name" | "avatar" | "!avatar" | "url" | "data" | "ipfs" |
  38. "erc721" | "erc1155" | "!erc721-caip" | "!erc1155-caip" |
  39. "!owner" | "owner" | "!balance" | "balance" |
  40. "metadata-url-base" | "metadata-url-expanded" | "metadata-url" | "!metadata-url" |
  41. "!metadata" | "metadata" |
  42. "!imageUrl" | "imageUrl-ipfs" | "imageUrl" | "!imageUrl-ipfs";
  43. /**
  44. * An individual record for each step during avatar resolution.
  45. */
  46. export interface AvatarLinkage {
  47. /**
  48. * The type of linkage.
  49. */
  50. type: AvatarLinkageType;
  51. /**
  52. * The linkage value.
  53. */
  54. value: string;
  55. };
  56. /**
  57. * When resolving an avatar for an ENS name, there are many
  58. * steps involved, fetching metadata, validating results, et cetera.
  59. *
  60. * Some applications may wish to analyse this data, or use this data
  61. * to diagnose promblems, so an **AvatarResult** provides details of
  62. * each completed step during avatar resolution.
  63. */
  64. export interface AvatarResult {
  65. /**
  66. * How the [[url]] was arrived at, resolving the many steps required
  67. * for an avatar URL.
  68. */
  69. linkage: Array<AvatarLinkage>;
  70. /**
  71. * The avatar URL or null if the avatar was not set, or there was
  72. * an issue during validation (such as the address not owning the
  73. * avatar or a metadata error).
  74. */
  75. url: null | string;
  76. };
  77. /**
  78. * A provider plugin super-class for processing multicoin address types.
  79. */
  80. export abstract class MulticoinProviderPlugin implements AbstractProviderPlugin {
  81. /**
  82. * The name.
  83. */
  84. readonly name!: string;
  85. /**
  86. * Creates a new **MulticoinProviderPluing** for %%name%%.
  87. */
  88. constructor(name: string) {
  89. defineProperties<MulticoinProviderPlugin>(this, { name });
  90. }
  91. connect(proivder: Provider): MulticoinProviderPlugin {
  92. return this;
  93. }
  94. /**
  95. * Returns ``true`` if %%coinType%% is supported by this plugin.
  96. */
  97. supportsCoinType(coinType: number): boolean {
  98. return false;
  99. }
  100. /**
  101. * Resolves to the encoded %%address%% for %%coinType%%.
  102. */
  103. async encodeAddress(coinType: number, address: string): Promise<string> {
  104. throw new Error("unsupported coin");
  105. }
  106. /**
  107. * Resolves to the decoded %%data%% for %%coinType%%.
  108. */
  109. async decodeAddress(coinType: number, data: BytesLike): Promise<string> {
  110. throw new Error("unsupported coin");
  111. }
  112. }
  113. const BasicMulticoinPluginId = "org.ethers.plugins.provider.BasicMulticoin";
  114. /**
  115. * A **BasicMulticoinProviderPlugin** provides service for common
  116. * coin types, which do not require additional libraries to encode or
  117. * decode.
  118. */
  119. export class BasicMulticoinProviderPlugin extends MulticoinProviderPlugin {
  120. /**
  121. * Creates a new **BasicMulticoinProviderPlugin**.
  122. */
  123. constructor() {
  124. super(BasicMulticoinPluginId);
  125. }
  126. }
  127. const matcherIpfs = new RegExp("^(ipfs):/\/(.*)$", "i");
  128. const matchers = [
  129. new RegExp("^(https):/\/(.*)$", "i"),
  130. new RegExp("^(data):(.*)$", "i"),
  131. matcherIpfs,
  132. new RegExp("^eip155:[0-9]+/(erc[0-9]+):(.*)$", "i"),
  133. ];
  134. /**
  135. * A connected object to a resolved ENS name resolver, which can be
  136. * used to query additional details.
  137. */
  138. export class EnsResolver {
  139. /**
  140. * The connected provider.
  141. */
  142. provider!: AbstractProvider;
  143. /**
  144. * The address of the resolver.
  145. */
  146. address!: string;
  147. /**
  148. * The name this resolver was resolved against.
  149. */
  150. name!: string;
  151. // For EIP-2544 names, the ancestor that provided the resolver
  152. #supports2544: null | Promise<boolean>;
  153. #resolver: Contract;
  154. constructor(provider: AbstractProvider, address: string, name: string) {
  155. defineProperties<EnsResolver>(this, { provider, address, name });
  156. this.#supports2544 = null;
  157. this.#resolver = new Contract(address, [
  158. "function supportsInterface(bytes4) view returns (bool)",
  159. "function resolve(bytes, bytes) view returns (bytes)",
  160. "function addr(bytes32) view returns (address)",
  161. "function addr(bytes32, uint) view returns (bytes)",
  162. "function text(bytes32, string) view returns (string)",
  163. "function contenthash(bytes32) view returns (bytes)",
  164. ], provider);
  165. }
  166. /**
  167. * Resolves to true if the resolver supports wildcard resolution.
  168. */
  169. async supportsWildcard(): Promise<boolean> {
  170. if (this.#supports2544 == null) {
  171. this.#supports2544 = (async () => {
  172. try {
  173. return await this.#resolver.supportsInterface("0x9061b923");
  174. } catch (error) {
  175. // Wildcard resolvers must understand supportsInterface
  176. // and return true.
  177. if (isError(error, "CALL_EXCEPTION")) { return false; }
  178. // Let future attempts try again...
  179. this.#supports2544 = null;
  180. throw error;
  181. }
  182. })();
  183. }
  184. return await this.#supports2544;
  185. }
  186. async #fetch(funcName: string, params?: Array<any>): Promise<null | any> {
  187. params = (params || []).slice();
  188. const iface = this.#resolver.interface;
  189. // The first parameters is always the nodehash
  190. params.unshift(namehash(this.name))
  191. let fragment: null | FunctionFragment = null;
  192. if (await this.supportsWildcard()) {
  193. fragment = iface.getFunction(funcName);
  194. assert(fragment, "missing fragment", "UNKNOWN_ERROR", {
  195. info: { funcName }
  196. });
  197. params = [
  198. dnsEncode(this.name, 255),
  199. iface.encodeFunctionData(fragment, params)
  200. ];
  201. funcName = "resolve(bytes,bytes)";
  202. }
  203. params.push({
  204. enableCcipRead: true
  205. });
  206. try {
  207. const result = await this.#resolver[funcName](...params);
  208. if (fragment) {
  209. return iface.decodeFunctionResult(fragment, result)[0];
  210. }
  211. return result;
  212. } catch (error: any) {
  213. if (!isError(error, "CALL_EXCEPTION")) { throw error; }
  214. }
  215. return null;
  216. }
  217. /**
  218. * Resolves to the address for %%coinType%% or null if the
  219. * provided %%coinType%% has not been configured.
  220. */
  221. async getAddress(coinType?: number): Promise<null | string> {
  222. if (coinType == null) { coinType = 60; }
  223. if (coinType === 60) {
  224. try {
  225. const result = await this.#fetch("addr(bytes32)");
  226. // No address
  227. if (result == null || result === ZeroAddress) { return null; }
  228. return result;
  229. } catch (error: any) {
  230. if (isError(error, "CALL_EXCEPTION")) { return null; }
  231. throw error;
  232. }
  233. }
  234. // Try decoding its EVM canonical chain as an EVM chain address first
  235. if (coinType >= 0 && coinType < 0x80000000) {
  236. let ethCoinType = coinType + 0x80000000;
  237. const data = await this.#fetch("addr(bytes32,uint)", [ ethCoinType ]);
  238. if (isHexString(data, 20)) { return getAddress(data); }
  239. }
  240. let coinPlugin: null | MulticoinProviderPlugin = null;
  241. for (const plugin of this.provider.plugins) {
  242. if (!(plugin instanceof MulticoinProviderPlugin)) { continue; }
  243. if (plugin.supportsCoinType(coinType)) {
  244. coinPlugin = plugin;
  245. break;
  246. }
  247. }
  248. if (coinPlugin == null) { return null; }
  249. // keccak256("addr(bytes32,uint256")
  250. const data = await this.#fetch("addr(bytes32,uint)", [ coinType ]);
  251. // No address
  252. if (data == null || data === "0x") { return null; }
  253. // Compute the address
  254. const address = await coinPlugin.decodeAddress(coinType, data);
  255. if (address != null) { return address; }
  256. assert(false, `invalid coin data`, "UNSUPPORTED_OPERATION", {
  257. operation: `getAddress(${ coinType })`,
  258. info: { coinType, data }
  259. });
  260. }
  261. /**
  262. * Resolves to the EIP-634 text record for %%key%%, or ``null``
  263. * if unconfigured.
  264. */
  265. async getText(key: string): Promise<null | string> {
  266. const data = await this.#fetch("text(bytes32,string)", [ key ]);
  267. if (data == null || data === "0x") { return null; }
  268. return data;
  269. }
  270. /**
  271. * Rsolves to the content-hash or ``null`` if unconfigured.
  272. */
  273. async getContentHash(): Promise<null | string> {
  274. // keccak256("contenthash()")
  275. const data = await this.#fetch("contenthash(bytes32)");
  276. // No contenthash
  277. if (data == null || data === "0x") { return null; }
  278. // IPFS (CID: 1, Type: 70=DAG-PB, 72=libp2p-key)
  279. const ipfs = data.match(/^0x(e3010170|e5010172)(([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f]*))$/);
  280. if (ipfs) {
  281. const scheme = (ipfs[1] === "e3010170") ? "ipfs": "ipns";
  282. const length = parseInt(ipfs[4], 16);
  283. if (ipfs[5].length === length * 2) {
  284. return `${ scheme }:/\/${ encodeBase58("0x" + ipfs[2])}`;
  285. }
  286. }
  287. // Swarm (CID: 1, Type: swarm-manifest; hash/length hard-coded to keccak256/32)
  288. const swarm = data.match(/^0xe40101fa011b20([0-9a-f]*)$/)
  289. if (swarm && swarm[1].length === 64) {
  290. return `bzz:/\/${ swarm[1] }`;
  291. }
  292. assert(false, `invalid or unsupported content hash data`, "UNSUPPORTED_OPERATION", {
  293. operation: "getContentHash()",
  294. info: { data }
  295. });
  296. }
  297. /**
  298. * Resolves to the avatar url or ``null`` if the avatar is either
  299. * unconfigured or incorrectly configured (e.g. references an NFT
  300. * not owned by the address).
  301. *
  302. * If diagnosing issues with configurations, the [[_getAvatar]]
  303. * method may be useful.
  304. */
  305. async getAvatar(): Promise<null | string> {
  306. const avatar = await this._getAvatar();
  307. return avatar.url;
  308. }
  309. /**
  310. * When resolving an avatar, there are many steps involved, such
  311. * fetching metadata and possibly validating ownership of an
  312. * NFT.
  313. *
  314. * This method can be used to examine each step and the value it
  315. * was working from.
  316. */
  317. async _getAvatar(): Promise<AvatarResult> {
  318. const linkage: Array<AvatarLinkage> = [ { type: "name", value: this.name } ];
  319. try {
  320. // test data for ricmoo.eth
  321. //const avatar = "eip155:1/erc721:0x265385c7f4132228A0d54EB1A9e7460b91c0cC68/29233";
  322. const avatar = await this.getText("avatar");
  323. if (avatar == null) {
  324. linkage.push({ type: "!avatar", value: "" });
  325. return { url: null, linkage };
  326. }
  327. linkage.push({ type: "avatar", value: avatar });
  328. for (let i = 0; i < matchers.length; i++) {
  329. const match = avatar.match(matchers[i]);
  330. if (match == null) { continue; }
  331. const scheme = match[1].toLowerCase();
  332. switch (scheme) {
  333. case "https":
  334. case "data":
  335. linkage.push({ type: "url", value: avatar });
  336. return { linkage, url: avatar };
  337. case "ipfs": {
  338. const url = getIpfsLink(avatar);
  339. linkage.push({ type: "ipfs", value: avatar });
  340. linkage.push({ type: "url", value: url });
  341. return { linkage, url };
  342. }
  343. case "erc721":
  344. case "erc1155": {
  345. // Depending on the ERC type, use tokenURI(uint256) or url(uint256)
  346. const selector = (scheme === "erc721") ? "tokenURI(uint256)": "uri(uint256)";
  347. linkage.push({ type: scheme, value: avatar });
  348. // The owner of this name
  349. const owner = await this.getAddress();
  350. if (owner == null) {
  351. linkage.push({ type: "!owner", value: "" });
  352. return { url: null, linkage };
  353. }
  354. const comps = (match[2] || "").split("/");
  355. if (comps.length !== 2) {
  356. linkage.push({ type: <any>`!${ scheme }caip`, value: (match[2] || "") });
  357. return { url: null, linkage };
  358. }
  359. const tokenId = comps[1];
  360. const contract = new Contract(comps[0], [
  361. // ERC-721
  362. "function tokenURI(uint) view returns (string)",
  363. "function ownerOf(uint) view returns (address)",
  364. // ERC-1155
  365. "function uri(uint) view returns (string)",
  366. "function balanceOf(address, uint256) view returns (uint)"
  367. ], this.provider);
  368. // Check that this account owns the token
  369. if (scheme === "erc721") {
  370. const tokenOwner = await contract.ownerOf(tokenId);
  371. if (owner !== tokenOwner) {
  372. linkage.push({ type: "!owner", value: tokenOwner });
  373. return { url: null, linkage };
  374. }
  375. linkage.push({ type: "owner", value: tokenOwner });
  376. } else if (scheme === "erc1155") {
  377. const balance = await contract.balanceOf(owner, tokenId);
  378. if (!balance) {
  379. linkage.push({ type: "!balance", value: "0" });
  380. return { url: null, linkage };
  381. }
  382. linkage.push({ type: "balance", value: balance.toString() });
  383. }
  384. // Call the token contract for the metadata URL
  385. let metadataUrl = await contract[selector](tokenId);
  386. if (metadataUrl == null || metadataUrl === "0x") {
  387. linkage.push({ type: "!metadata-url", value: "" });
  388. return { url: null, linkage };
  389. }
  390. linkage.push({ type: "metadata-url-base", value: metadataUrl });
  391. // ERC-1155 allows a generic {id} in the URL
  392. if (scheme === "erc1155") {
  393. metadataUrl = metadataUrl.replace("{id}", toBeHex(tokenId, 32).substring(2));
  394. linkage.push({ type: "metadata-url-expanded", value: metadataUrl });
  395. }
  396. // Transform IPFS metadata links
  397. if (metadataUrl.match(/^ipfs:/i)) {
  398. metadataUrl = getIpfsLink(metadataUrl);
  399. }
  400. linkage.push({ type: "metadata-url", value: metadataUrl });
  401. // Get the token metadata
  402. let metadata: any = { };
  403. const response = await (new FetchRequest(metadataUrl)).send();
  404. response.assertOk();
  405. try {
  406. metadata = response.bodyJson;
  407. } catch (error) {
  408. try {
  409. linkage.push({ type: "!metadata", value: response.bodyText });
  410. } catch (error) {
  411. const bytes = response.body;
  412. if (bytes) {
  413. linkage.push({ type: "!metadata", value: hexlify(bytes) });
  414. }
  415. return { url: null, linkage };
  416. }
  417. return { url: null, linkage };
  418. }
  419. if (!metadata) {
  420. linkage.push({ type: "!metadata", value: "" });
  421. return { url: null, linkage };
  422. }
  423. linkage.push({ type: "metadata", value: JSON.stringify(metadata) });
  424. // Pull the image URL out
  425. let imageUrl = metadata.image;
  426. if (typeof(imageUrl) !== "string") {
  427. linkage.push({ type: "!imageUrl", value: "" });
  428. return { url: null, linkage };
  429. }
  430. if (imageUrl.match(/^(https:\/\/|data:)/i)) {
  431. // Allow
  432. } else {
  433. // Transform IPFS link to gateway
  434. const ipfs = imageUrl.match(matcherIpfs);
  435. if (ipfs == null) {
  436. linkage.push({ type: "!imageUrl-ipfs", value: imageUrl });
  437. return { url: null, linkage };
  438. }
  439. linkage.push({ type: "imageUrl-ipfs", value: imageUrl });
  440. imageUrl = getIpfsLink(imageUrl);
  441. }
  442. linkage.push({ type: "url", value: imageUrl });
  443. return { linkage, url: imageUrl };
  444. }
  445. }
  446. }
  447. } catch (error) { }
  448. return { linkage, url: null };
  449. }
  450. static async getEnsAddress(provider: Provider): Promise<string> {
  451. const network = await provider.getNetwork();
  452. const ensPlugin = network.getPlugin<EnsPlugin>("org.ethers.plugins.network.Ens");
  453. // No ENS...
  454. assert(ensPlugin, "network does not support ENS", "UNSUPPORTED_OPERATION", {
  455. operation: "getEnsAddress", info: { network } });
  456. return ensPlugin.address;
  457. }
  458. static async #getResolver(provider: Provider, name: string): Promise<null | string> {
  459. const ensAddr = await EnsResolver.getEnsAddress(provider);
  460. try {
  461. const contract = new Contract(ensAddr, [
  462. "function resolver(bytes32) view returns (address)"
  463. ], provider);
  464. const addr = await contract.resolver(namehash(name), {
  465. enableCcipRead: true
  466. });
  467. if (addr === ZeroAddress) { return null; }
  468. return addr;
  469. } catch (error) {
  470. // ENS registry cannot throw errors on resolver(bytes32),
  471. // so probably a link error
  472. throw error;
  473. }
  474. return null;
  475. }
  476. /**
  477. * Resolve to the ENS resolver for %%name%% using %%provider%% or
  478. * ``null`` if unconfigured.
  479. */
  480. static async fromName(provider: AbstractProvider, name: string): Promise<null | EnsResolver> {
  481. let currentName = name;
  482. while (true) {
  483. if (currentName === "" || currentName === ".") { return null; }
  484. // Optimization since the eth node cannot change and does
  485. // not have a wildcard resolver
  486. if (name !== "eth" && currentName === "eth") { return null; }
  487. // Check the current node for a resolver
  488. const addr = await EnsResolver.#getResolver(provider, currentName);
  489. // Found a resolver!
  490. if (addr != null) {
  491. const resolver = new EnsResolver(provider, addr, name);
  492. // Legacy resolver found, using EIP-2544 so it isn't safe to use
  493. if (currentName !== name && !(await resolver.supportsWildcard())) { return null; }
  494. return resolver;
  495. }
  496. // Get the parent node
  497. currentName = currentName.split(".").slice(1).join(".");
  498. }
  499. }
  500. }