ens-resolver.js 20 KB

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