ethers.js/lib.commonjs/providers/ens-resolver.js

502 lines
20 KiB
JavaScript
Raw Permalink Normal View History

2022-09-05 16:57:11 -04:00
"use strict";
2022-12-09 18:24:58 -05:00
/**
2023-06-01 17:52:58 -04:00
* ENS is a service which allows easy-to-remember names to map to
* network addresses.
2022-12-09 18:24:58 -05:00
*
* @_section: api/providers/ens-resolver:ENS Resolver [about-ens-rsolver]
*/
2022-09-05 16:57:11 -04:00
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnsResolver = exports.BasicMulticoinProviderPlugin = exports.MulticoinProviderPlugin = void 0;
2023-06-13 21:47:44 -04:00
const index_js_1 = require("../address/index.js");
const index_js_2 = require("../constants/index.js");
const index_js_3 = require("../contract/index.js");
const index_js_4 = require("../hash/index.js");
const index_js_5 = require("../utils/index.js");
2022-09-05 16:57:11 -04:00
// @TODO: This should use the fetch-data:ipfs gateway
// Trim off the ipfs:// prefix and return the default gateway URL
function getIpfsLink(link) {
if (link.match(/^ipfs:\/\/ipfs\//i)) {
link = link.substring(12);
}
else if (link.match(/^ipfs:\/\//i)) {
link = link.substring(7);
}
else {
2023-06-13 21:47:44 -04:00
(0, index_js_5.assertArgument)(false, "unsupported IPFS format", "link", link);
2022-09-05 16:57:11 -04:00
}
return `https:/\/gateway.ipfs.io/ipfs/${link}`;
}
;
;
2022-12-09 18:24:58 -05:00
/**
* A provider plugin super-class for processing multicoin address types.
*/
2022-09-05 16:57:11 -04:00
class MulticoinProviderPlugin {
2023-06-01 17:52:58 -04:00
/**
* The name.
*/
2022-09-05 16:57:11 -04:00
name;
2023-06-01 17:52:58 -04:00
/**
* Creates a new **MulticoinProviderPluing** for %%name%%.
*/
2022-09-05 16:57:11 -04:00
constructor(name) {
2023-06-13 21:47:44 -04:00
(0, index_js_5.defineProperties)(this, { name });
2022-09-05 16:57:11 -04:00
}
2022-11-09 02:57:02 -05:00
connect(proivder) {
2022-09-05 16:57:11 -04:00
return this;
}
2023-06-01 17:52:58 -04:00
/**
* Returns ``true`` if %%coinType%% is supported by this plugin.
*/
2022-09-05 16:57:11 -04:00
supportsCoinType(coinType) {
return false;
}
2023-06-01 17:52:58 -04:00
/**
* Resovles to the encoded %%address%% for %%coinType%%.
*/
2022-09-05 16:57:11 -04:00
async encodeAddress(coinType, address) {
throw new Error("unsupported coin");
}
2023-06-01 17:52:58 -04:00
/**
* Resovles to the decoded %%data%% for %%coinType%%.
*/
2022-09-05 16:57:11 -04:00
async decodeAddress(coinType, data) {
throw new Error("unsupported coin");
}
}
exports.MulticoinProviderPlugin = MulticoinProviderPlugin;
2023-02-02 04:05:47 -05:00
const BasicMulticoinPluginId = "org.ethers.plugins.provider.BasicMulticoin";
2022-12-09 18:24:58 -05:00
/**
2023-06-01 17:52:58 -04:00
* A **BasicMulticoinProviderPlugin** provides service for common
* coin types, which do not require additional libraries to encode or
* decode.
2022-12-09 18:24:58 -05:00
*/
2022-09-05 16:57:11 -04:00
class BasicMulticoinProviderPlugin extends MulticoinProviderPlugin {
2023-06-01 17:52:58 -04:00
/**
* Creates a new **BasicMulticoinProviderPlugin**.
*/
2022-09-05 16:57:11 -04:00
constructor() {
super(BasicMulticoinPluginId);
}
}
exports.BasicMulticoinProviderPlugin = BasicMulticoinProviderPlugin;
const matcherIpfs = new RegExp("^(ipfs):/\/(.*)$", "i");
const matchers = [
new RegExp("^(https):/\/(.*)$", "i"),
new RegExp("^(data):(.*)$", "i"),
matcherIpfs,
new RegExp("^eip155:[0-9]+/(erc[0-9]+):(.*)$", "i"),
];
2022-12-09 18:24:58 -05:00
/**
* A connected object to a resolved ENS name resolver, which can be
* used to query additional details.
*/
2022-09-05 16:57:11 -04:00
class EnsResolver {
2022-12-09 18:24:58 -05:00
/**
* The connected provider.
*/
2022-09-05 16:57:11 -04:00
provider;
2022-12-09 18:24:58 -05:00
/**
* The address of the resolver.
*/
2022-09-05 16:57:11 -04:00
address;
2022-12-09 18:24:58 -05:00
/**
2023-03-20 12:53:37 -04:00
* The name this resolver was resolved against.
2022-12-09 18:24:58 -05:00
*/
2022-09-05 16:57:11 -04:00
name;
// For EIP-2544 names, the ancestor that provided the resolver
#supports2544;
2023-02-02 04:05:47 -05:00
#resolver;
2022-09-05 16:57:11 -04:00
constructor(provider, address, name) {
2023-06-13 21:47:44 -04:00
(0, index_js_5.defineProperties)(this, { provider, address, name });
2022-09-05 16:57:11 -04:00
this.#supports2544 = null;
2023-06-13 21:47:44 -04:00
this.#resolver = new index_js_3.Contract(address, [
2023-02-02 04:05:47 -05:00
"function supportsInterface(bytes4) view returns (bool)",
"function resolve(bytes, bytes) view returns (bytes)",
"function addr(bytes32) view returns (address)",
2023-06-13 21:47:44 -04:00
"function addr(bytes32, uint) view returns (bytes)",
2023-02-02 04:05:47 -05:00
"function text(bytes32, string) view returns (string)",
2023-02-23 01:41:13 -05:00
"function contenthash(bytes32) view returns (bytes)",
2023-02-02 04:05:47 -05:00
], provider);
2022-09-05 16:57:11 -04:00
}
2022-12-09 18:24:58 -05:00
/**
* Resolves to true if the resolver supports wildcard resolution.
*/
2022-09-05 16:57:11 -04:00
async supportsWildcard() {
2023-02-02 04:05:47 -05:00
if (this.#supports2544 == null) {
this.#supports2544 = (async () => {
try {
return await this.#resolver.supportsInterface("0x9061b923");
2022-09-05 16:57:11 -04:00
}
2023-02-02 04:05:47 -05:00
catch (error) {
// Wildcard resolvers must understand supportsInterface
// and return true.
2023-06-13 21:47:44 -04:00
if ((0, index_js_5.isError)(error, "CALL_EXCEPTION")) {
2023-02-02 04:05:47 -05:00
return false;
}
// Let future attempts try again...
this.#supports2544 = null;
throw error;
}
})();
2022-09-05 16:57:11 -04:00
}
return await this.#supports2544;
}
2023-02-02 04:05:47 -05:00
async #fetch(funcName, params) {
params = (params || []).slice();
const iface = this.#resolver.interface;
// The first parameters is always the nodehash
2023-06-13 21:47:44 -04:00
params.unshift((0, index_js_4.namehash)(this.name));
2023-02-02 04:05:47 -05:00
let fragment = null;
2022-09-05 16:57:11 -04:00
if (await this.supportsWildcard()) {
2023-02-02 04:05:47 -05:00
fragment = iface.getFunction(funcName);
2023-06-13 21:47:44 -04:00
(0, index_js_5.assert)(fragment, "missing fragment", "UNKNOWN_ERROR", {
2023-02-02 04:05:47 -05:00
info: { funcName }
});
params = [
2023-06-13 21:47:44 -04:00
(0, index_js_4.dnsEncode)(this.name),
2023-02-02 04:05:47 -05:00
iface.encodeFunctionData(fragment, params)
];
funcName = "resolve(bytes,bytes)";
2022-09-05 16:57:11 -04:00
}
2023-02-02 04:05:47 -05:00
params.push({
2023-07-15 17:55:24 -04:00
enableCcipRead: true
2023-02-02 04:05:47 -05:00
});
2022-09-05 16:57:11 -04:00
try {
2023-02-02 04:05:47 -05:00
const result = await this.#resolver[funcName](...params);
if (fragment) {
return iface.decodeFunctionResult(fragment, result)[0];
2022-09-05 16:57:11 -04:00
}
2023-02-02 04:05:47 -05:00
return result;
2022-09-05 16:57:11 -04:00
}
catch (error) {
2023-06-13 21:47:44 -04:00
if (!(0, index_js_5.isError)(error, "CALL_EXCEPTION")) {
2022-09-05 16:57:11 -04:00
throw error;
}
}
return null;
}
2022-12-09 18:24:58 -05:00
/**
* Resolves to the address for %%coinType%% or null if the
* provided %%coinType%% has not been configured.
*/
2022-11-30 15:44:23 -05:00
async getAddress(coinType) {
if (coinType == null) {
coinType = 60;
}
2022-09-05 16:57:11 -04:00
if (coinType === 60) {
try {
2023-02-02 04:05:47 -05:00
const result = await this.#fetch("addr(bytes32)");
2022-09-05 16:57:11 -04:00
// No address
2023-06-13 21:47:44 -04:00
if (result == null || result === index_js_2.ZeroAddress) {
2022-09-05 16:57:11 -04:00
return null;
}
2023-02-02 04:05:47 -05:00
return result;
2022-09-05 16:57:11 -04:00
}
catch (error) {
2023-06-13 21:47:44 -04:00
if ((0, index_js_5.isError)(error, "CALL_EXCEPTION")) {
2022-09-05 16:57:11 -04:00
return null;
}
throw error;
}
}
2023-06-13 21:47:44 -04:00
// Try decoding its EVM canonical chain as an EVM chain address first
if (coinType >= 0 && coinType < 0x80000000) {
let ethCoinType = coinType + 0x80000000;
const data = await this.#fetch("addr(bytes32,uint)", [ethCoinType]);
if ((0, index_js_5.isHexString)(data, 20)) {
return (0, index_js_1.getAddress)(data);
}
}
2022-09-05 16:57:11 -04:00
let coinPlugin = null;
for (const plugin of this.provider.plugins) {
if (!(plugin instanceof MulticoinProviderPlugin)) {
continue;
}
if (plugin.supportsCoinType(coinType)) {
coinPlugin = plugin;
break;
}
}
if (coinPlugin == null) {
return null;
}
// keccak256("addr(bytes32,uint256")
2023-02-02 04:05:47 -05:00
const data = await this.#fetch("addr(bytes32,uint)", [coinType]);
2022-09-05 16:57:11 -04:00
// No address
if (data == null || data === "0x") {
return null;
}
// Compute the address
2023-06-13 21:47:44 -04:00
const address = await coinPlugin.decodeAddress(coinType, data);
2022-09-05 16:57:11 -04:00
if (address != null) {
return address;
}
2023-06-13 21:47:44 -04:00
(0, index_js_5.assert)(false, `invalid coin data`, "UNSUPPORTED_OPERATION", {
2022-09-05 16:57:11 -04:00
operation: `getAddress(${coinType})`,
info: { coinType, data }
});
}
2022-12-09 18:24:58 -05:00
/**
2023-06-07 20:20:11 -04:00
* Resolves to the EIP-634 text record for %%key%%, or ``null``
2022-12-09 18:24:58 -05:00
* if unconfigured.
*/
2022-09-05 16:57:11 -04:00
async getText(key) {
2023-02-02 04:05:47 -05:00
const data = await this.#fetch("text(bytes32,string)", [key]);
if (data == null || data === "0x") {
2022-09-05 16:57:11 -04:00
return null;
}
2023-02-02 04:05:47 -05:00
return data;
2022-09-05 16:57:11 -04:00
}
2022-12-09 18:24:58 -05:00
/**
* Rsolves to the content-hash or ``null`` if unconfigured.
*/
2022-09-05 16:57:11 -04:00
async getContentHash() {
// keccak256("contenthash()")
2023-02-23 01:41:13 -05:00
const data = await this.#fetch("contenthash(bytes32)");
2022-09-05 16:57:11 -04:00
// No contenthash
2023-02-02 04:05:47 -05:00
if (data == null || data === "0x") {
2022-09-05 16:57:11 -04:00
return null;
}
// IPFS (CID: 1, Type: 70=DAG-PB, 72=libp2p-key)
2023-02-02 04:05:47 -05:00
const ipfs = data.match(/^0x(e3010170|e5010172)(([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f]*))$/);
2022-09-05 16:57:11 -04:00
if (ipfs) {
const scheme = (ipfs[1] === "e3010170") ? "ipfs" : "ipns";
const length = parseInt(ipfs[4], 16);
if (ipfs[5].length === length * 2) {
2023-06-13 21:47:44 -04:00
return `${scheme}:/\/${(0, index_js_5.encodeBase58)("0x" + ipfs[2])}`;
2022-09-05 16:57:11 -04:00
}
}
// Swarm (CID: 1, Type: swarm-manifest; hash/length hard-coded to keccak256/32)
2023-02-02 04:05:47 -05:00
const swarm = data.match(/^0xe40101fa011b20([0-9a-f]*)$/);
2022-09-05 16:57:11 -04:00
if (swarm && swarm[1].length === 64) {
return `bzz:/\/${swarm[1]}`;
}
2023-06-13 21:47:44 -04:00
(0, index_js_5.assert)(false, `invalid or unsupported content hash data`, "UNSUPPORTED_OPERATION", {
2022-09-05 16:57:11 -04:00
operation: "getContentHash()",
2023-02-02 04:05:47 -05:00
info: { data }
2022-09-05 16:57:11 -04:00
});
}
2022-12-09 18:24:58 -05:00
/**
* Resolves to the avatar url or ``null`` if the avatar is either
* unconfigured or incorrectly configured (e.g. references an NFT
* not owned by the address).
*
* If diagnosing issues with configurations, the [[_getAvatar]]
* method may be useful.
*/
2022-09-05 16:57:11 -04:00
async getAvatar() {
2023-02-02 04:05:47 -05:00
const avatar = await this._getAvatar();
return avatar.url;
2022-09-05 16:57:11 -04:00
}
2022-12-09 18:24:58 -05:00
/**
* When resolving an avatar, there are many steps involved, such
* fetching metadata and possibly validating ownership of an
* NFT.
*
* This method can be used to examine each step and the value it
* was working from.
*/
2022-09-05 16:57:11 -04:00
async _getAvatar() {
const linkage = [{ type: "name", value: this.name }];
try {
// test data for ricmoo.eth
//const avatar = "eip155:1/erc721:0x265385c7f4132228A0d54EB1A9e7460b91c0cC68/29233";
const avatar = await this.getText("avatar");
if (avatar == null) {
linkage.push({ type: "!avatar", value: "" });
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
linkage.push({ type: "avatar", value: avatar });
for (let i = 0; i < matchers.length; i++) {
const match = avatar.match(matchers[i]);
if (match == null) {
continue;
}
const scheme = match[1].toLowerCase();
switch (scheme) {
case "https":
case "data":
linkage.push({ type: "url", value: avatar });
return { linkage, url: avatar };
case "ipfs": {
const url = getIpfsLink(avatar);
linkage.push({ type: "ipfs", value: avatar });
linkage.push({ type: "url", value: url });
return { linkage, url };
}
case "erc721":
case "erc1155": {
// Depending on the ERC type, use tokenURI(uint256) or url(uint256)
2023-02-02 04:05:47 -05:00
const selector = (scheme === "erc721") ? "tokenURI(uint256)" : "uri(uint256)";
2022-09-05 16:57:11 -04:00
linkage.push({ type: scheme, value: avatar });
// The owner of this name
const owner = await this.getAddress();
if (owner == null) {
linkage.push({ type: "!owner", value: "" });
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
const comps = (match[2] || "").split("/");
if (comps.length !== 2) {
linkage.push({ type: `!${scheme}caip`, value: (match[2] || "") });
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
2023-02-02 04:05:47 -05:00
const tokenId = comps[1];
2023-06-13 21:47:44 -04:00
const contract = new index_js_3.Contract(comps[0], [
2023-02-02 04:05:47 -05:00
// ERC-721
"function tokenURI(uint) view returns (string)",
"function ownerOf(uint) view returns (address)",
// ERC-1155
"function uri(uint) view returns (string)",
"function balanceOf(address, uint256) view returns (uint)"
], this.provider);
2022-09-05 16:57:11 -04:00
// Check that this account owns the token
if (scheme === "erc721") {
2023-02-02 04:05:47 -05:00
const tokenOwner = await contract.ownerOf(tokenId);
2022-09-05 16:57:11 -04:00
if (owner !== tokenOwner) {
linkage.push({ type: "!owner", value: tokenOwner });
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
linkage.push({ type: "owner", value: tokenOwner });
}
else if (scheme === "erc1155") {
2023-02-02 04:05:47 -05:00
const balance = await contract.balanceOf(owner, tokenId);
2022-09-05 16:57:11 -04:00
if (!balance) {
linkage.push({ type: "!balance", value: "0" });
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
linkage.push({ type: "balance", value: balance.toString() });
}
// Call the token contract for the metadata URL
2023-02-02 04:05:47 -05:00
let metadataUrl = await contract[selector](tokenId);
if (metadataUrl == null || metadataUrl === "0x") {
2022-09-05 16:57:11 -04:00
linkage.push({ type: "!metadata-url", value: "" });
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
linkage.push({ type: "metadata-url-base", value: metadataUrl });
// ERC-1155 allows a generic {id} in the URL
if (scheme === "erc1155") {
2023-06-13 21:47:44 -04:00
metadataUrl = metadataUrl.replace("{id}", (0, index_js_5.toBeHex)(tokenId, 32).substring(2));
2022-09-05 16:57:11 -04:00
linkage.push({ type: "metadata-url-expanded", value: metadataUrl });
}
// Transform IPFS metadata links
if (metadataUrl.match(/^ipfs:/i)) {
metadataUrl = getIpfsLink(metadataUrl);
}
linkage.push({ type: "metadata-url", value: metadataUrl });
// Get the token metadata
let metadata = {};
2023-06-13 21:47:44 -04:00
const response = await (new index_js_5.FetchRequest(metadataUrl)).send();
2022-09-05 16:57:11 -04:00
response.assertOk();
try {
metadata = response.bodyJson;
}
catch (error) {
try {
linkage.push({ type: "!metadata", value: response.bodyText });
}
catch (error) {
const bytes = response.body;
if (bytes) {
2023-06-13 21:47:44 -04:00
linkage.push({ type: "!metadata", value: (0, index_js_5.hexlify)(bytes) });
2022-09-05 16:57:11 -04:00
}
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
if (!metadata) {
linkage.push({ type: "!metadata", value: "" });
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
linkage.push({ type: "metadata", value: JSON.stringify(metadata) });
// Pull the image URL out
let imageUrl = metadata.image;
if (typeof (imageUrl) !== "string") {
linkage.push({ type: "!imageUrl", value: "" });
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
if (imageUrl.match(/^(https:\/\/|data:)/i)) {
// Allow
}
else {
// Transform IPFS link to gateway
const ipfs = imageUrl.match(matcherIpfs);
if (ipfs == null) {
linkage.push({ type: "!imageUrl-ipfs", value: imageUrl });
2023-02-02 04:05:47 -05:00
return { url: null, linkage };
2022-09-05 16:57:11 -04:00
}
linkage.push({ type: "imageUrl-ipfs", value: imageUrl });
imageUrl = getIpfsLink(imageUrl);
}
linkage.push({ type: "url", value: imageUrl });
return { linkage, url: imageUrl };
}
}
}
}
2023-02-02 04:05:47 -05:00
catch (error) { }
2022-09-05 16:57:11 -04:00
return { linkage, url: null };
}
2023-02-02 04:05:47 -05:00
static async getEnsAddress(provider) {
2022-09-05 16:57:11 -04:00
const network = await provider.getNetwork();
2023-02-02 04:05:47 -05:00
const ensPlugin = network.getPlugin("org.ethers.plugins.network.Ens");
2022-09-05 16:57:11 -04:00
// No ENS...
2023-06-13 21:47:44 -04:00
(0, index_js_5.assert)(ensPlugin, "network does not support ENS", "UNSUPPORTED_OPERATION", {
2023-02-02 04:05:47 -05:00
operation: "getEnsAddress", info: { network }
2022-11-09 02:57:02 -05:00
});
2023-02-02 04:05:47 -05:00
return ensPlugin.address;
}
static async #getResolver(provider, name) {
const ensAddr = await EnsResolver.getEnsAddress(provider);
2022-09-05 16:57:11 -04:00
try {
2023-06-13 21:47:44 -04:00
const contract = new index_js_3.Contract(ensAddr, [
2023-02-02 04:05:47 -05:00
"function resolver(bytes32) view returns (address)"
], provider);
2023-06-13 21:47:44 -04:00
const addr = await contract.resolver((0, index_js_4.namehash)(name), {
2022-09-05 16:57:11 -04:00
enableCcipRead: true
});
2023-06-13 21:47:44 -04:00
if (addr === index_js_2.ZeroAddress) {
2022-09-05 16:57:11 -04:00
return null;
}
return addr;
}
catch (error) {
// ENS registry cannot throw errors on resolver(bytes32),
// so probably a link error
throw error;
}
return null;
}
2022-12-09 18:24:58 -05:00
/**
* Resolve to the ENS resolver for %%name%% using %%provider%% or
2023-02-02 04:05:47 -05:00
* ``null`` if unconfigured.
2022-12-09 18:24:58 -05:00
*/
2022-09-05 16:57:11 -04:00
static async fromName(provider, name) {
let currentName = name;
while (true) {
if (currentName === "" || currentName === ".") {
return null;
}
// Optimization since the eth node cannot change and does
2023-06-06 22:42:28 -04:00
// not have a wildcard resolver
2022-09-05 16:57:11 -04:00
if (name !== "eth" && currentName === "eth") {
return null;
}
// Check the current node for a resolver
const addr = await EnsResolver.#getResolver(provider, currentName);
// Found a resolver!
if (addr != null) {
const resolver = new EnsResolver(provider, addr, name);
// Legacy resolver found, using EIP-2544 so it isn't safe to use
if (currentName !== name && !(await resolver.supportsWildcard())) {
return null;
}
return resolver;
}
// Get the parent node
currentName = currentName.split(".").slice(1).join(".");
}
}
}
exports.EnsResolver = EnsResolver;
//# sourceMappingURL=ens-resolver.js.map