ethers.js/lib.commonjs/providers/ens-resolver.js
2022-12-09 18:24:58 -05:00

537 lines
21 KiB
JavaScript

"use strict";
/**
* About ENS Resolver
*
* @_section: api/providers/ens-resolver:ENS Resolver [about-ens-rsolver]
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnsResolver = exports.BasicMulticoinProviderPlugin = exports.MulticoinProviderPlugin = void 0;
const index_js_1 = require("../address/index.js");
const index_js_2 = require("../constants/index.js");
const index_js_3 = require("../hash/index.js");
const index_js_4 = require("../utils/index.js");
const BN_1 = BigInt(1);
const Empty = new Uint8Array([]);
function parseBytes(result, start) {
if (result === "0x") {
return null;
}
const offset = (0, index_js_4.toNumber)((0, index_js_4.dataSlice)(result, start, start + 32));
const length = (0, index_js_4.toNumber)((0, index_js_4.dataSlice)(result, offset, offset + 32));
return (0, index_js_4.dataSlice)(result, offset + 32, offset + 32 + length);
}
function parseString(result, start) {
try {
const bytes = parseBytes(result, start);
if (bytes != null) {
return (0, index_js_4.toUtf8String)(bytes);
}
}
catch (error) { }
return null;
}
function numPad(value) {
const result = (0, index_js_4.toBeArray)(value);
if (result.length > 32) {
throw new Error("internal; should not happen");
}
const padded = new Uint8Array(32);
padded.set(result, 32 - result.length);
return padded;
}
function bytesPad(value) {
if ((value.length % 32) === 0) {
return value;
}
const result = new Uint8Array(Math.ceil(value.length / 32) * 32);
result.set(value);
return result;
}
// ABI Encodes a series of (bytes, bytes, ...)
function encodeBytes(datas) {
const result = [];
let byteCount = 0;
// Add place-holders for pointers as we add items
for (let i = 0; i < datas.length; i++) {
result.push(Empty);
byteCount += 32;
}
for (let i = 0; i < datas.length; i++) {
const data = (0, index_js_4.getBytes)(datas[i]);
// Update the bytes offset
result[i] = numPad(byteCount);
// The length and padded value of data
result.push(numPad(data.length));
result.push(bytesPad(data));
byteCount += 32 + Math.ceil(data.length / 32) * 32;
}
return (0, index_js_4.concat)(result);
}
function callAddress(value) {
(0, index_js_4.assertArgument)(value.length === 66 && (0, index_js_4.dataSlice)(value, 0, 12) === "0x000000000000000000000000", "invalid call address", "value", value);
return (0, index_js_1.getAddress)("0x" + value.substring(26));
}
// @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 {
(0, index_js_4.assertArgument)(false, "unsupported IPFS format", "link", link);
}
return `https:/\/gateway.ipfs.io/ipfs/${link}`;
}
;
;
/**
* A provider plugin super-class for processing multicoin address types.
*/
class MulticoinProviderPlugin {
name;
constructor(name) {
(0, index_js_4.defineProperties)(this, { name });
}
connect(proivder) {
return this;
}
supportsCoinType(coinType) {
return false;
}
async encodeAddress(coinType, address) {
throw new Error("unsupported coin");
}
async decodeAddress(coinType, data) {
throw new Error("unsupported coin");
}
}
exports.MulticoinProviderPlugin = MulticoinProviderPlugin;
const BasicMulticoinPluginId = "org.ethers.plugins.BasicMulticoinProviderPlugin";
/**
* A basic multicoin provider plugin.
*/
class BasicMulticoinProviderPlugin extends MulticoinProviderPlugin {
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"),
];
/**
* A connected object to a resolved ENS name resolver, which can be
* used to query additional details.
*/
class EnsResolver {
/**
* The connected provider.
*/
provider;
/**
* The address of the resolver.
*/
address;
/**
* The name this resovler was resolved against.
*/
name;
// For EIP-2544 names, the ancestor that provided the resolver
#supports2544;
constructor(provider, address, name) {
(0, index_js_4.defineProperties)(this, { provider, address, name });
this.#supports2544 = null;
}
/**
* Resolves to true if the resolver supports wildcard resolution.
*/
async supportsWildcard() {
if (!this.#supports2544) {
// supportsInterface(bytes4 = selector("resolve(bytes,bytes)"))
this.#supports2544 = this.provider.call({
to: this.address,
data: "0x01ffc9a79061b92300000000000000000000000000000000000000000000000000000000"
}).then((result) => {
return ((0, index_js_4.getBigInt)(result) === BN_1);
}).catch((error) => {
if (error.code === "CALL_EXCEPTION") {
return false;
}
// Rethrow the error: link is down, etc. Let future attempts retry.
this.#supports2544 = null;
throw error;
});
}
return await this.#supports2544;
}
/**
* Fetch the %%selector%% with %%parameters%% using call, resolving
* recursively if the resolver supports it.
*/
async _fetch(selector, parameters) {
if (parameters == null) {
parameters = "0x";
}
// e.g. keccak256("addr(bytes32,uint256)")
const addrData = (0, index_js_4.concat)([selector, (0, index_js_3.namehash)(this.name), parameters]);
const tx = {
to: this.address,
from: index_js_2.ZeroAddress,
enableCcipRead: true,
data: addrData
};
// Wildcard support; use EIP-2544 to resolve the request
let wrapped = false;
if (await this.supportsWildcard()) {
wrapped = true;
// selector("resolve(bytes,bytes)")
tx.data = (0, index_js_4.concat)(["0x9061b923", encodeBytes([(0, index_js_3.dnsEncode)(this.name), addrData])]);
}
try {
let data = await this.provider.call(tx);
(0, index_js_4.assert)(((0, index_js_4.getBytes)(data).length % 32) !== 4, "execution reverted during JSON-RPC call (could not parse reason; invalid data length)", "CALL_EXCEPTION", {
action: "call", data, reason: null, transaction: tx,
invocation: null, revert: null
});
if (wrapped) {
return parseBytes(data, 0);
}
return data;
}
catch (error) {
if (error.code !== "CALL_EXCEPTION") {
throw error;
}
}
return null;
}
/**
* Resolves to the address for %%coinType%% or null if the
* provided %%coinType%% has not been configured.
*/
async getAddress(coinType) {
if (coinType == null) {
coinType = 60;
}
if (coinType === 60) {
try {
// keccak256("addr(bytes32)")
const result = await this._fetch("0x3b3b57de");
// No address
if (result == null || result === "0x" || result === index_js_2.ZeroHash) {
return null;
}
return callAddress(result);
}
catch (error) {
if (error.code === "CALL_EXCEPTION") {
return null;
}
throw error;
}
}
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")
const data = parseBytes((await this._fetch("0xf1cb7e06", numPad(coinType))) || "0x", 0);
// No address
if (data == null || data === "0x") {
return null;
}
// Compute the address
const address = await coinPlugin.encodeAddress(coinType, data);
if (address != null) {
return address;
}
(0, index_js_4.assert)(false, `invalid coin data`, "UNSUPPORTED_OPERATION", {
operation: `getAddress(${coinType})`,
info: { coinType, data }
});
}
/**
* Resovles to the EIP-643 text record for %%key%%, or ``null``
* if unconfigured.
*/
async getText(key) {
// The key encoded as parameter to fetchBytes
let keyBytes = (0, index_js_4.toUtf8Bytes)(key);
// The nodehash consumes the first slot, so the string pointer targets
// offset 64, with the length at offset 64 and data starting at offset 96
const calldata = (0, index_js_4.getBytes)((0, index_js_4.concat)([numPad(64), numPad(keyBytes.length), keyBytes]));
const hexBytes = parseBytes((await this._fetch("0x59d1d43c", bytesPad(calldata))) || "0x", 0);
if (hexBytes == null || hexBytes === "0x") {
return null;
}
return (0, index_js_4.toUtf8String)(hexBytes);
}
/**
* Rsolves to the content-hash or ``null`` if unconfigured.
*/
async getContentHash() {
// keccak256("contenthash()")
const hexBytes = parseBytes((await this._fetch("0xbc1c58d1")) || "0x", 0);
// No contenthash
if (hexBytes == null || hexBytes === "0x") {
return null;
}
// IPFS (CID: 1, Type: 70=DAG-PB, 72=libp2p-key)
const ipfs = hexBytes.match(/^0x(e3010170|e5010172)(([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f]*))$/);
if (ipfs) {
const scheme = (ipfs[1] === "e3010170") ? "ipfs" : "ipns";
const length = parseInt(ipfs[4], 16);
if (ipfs[5].length === length * 2) {
return `${scheme}:/\/${(0, index_js_4.encodeBase58)("0x" + ipfs[2])}`;
}
}
// Swarm (CID: 1, Type: swarm-manifest; hash/length hard-coded to keccak256/32)
const swarm = hexBytes.match(/^0xe40101fa011b20([0-9a-f]*)$/);
if (swarm && swarm[1].length === 64) {
return `bzz:/\/${swarm[1]}`;
}
(0, index_js_4.assert)(false, `invalid or unsupported content hash data`, "UNSUPPORTED_OPERATION", {
operation: "getContentHash()",
info: { data: hexBytes }
});
}
/**
* 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.
*/
async getAvatar() {
return (await this._getAvatar()).url;
}
/**
* 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.
*/
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: "" });
throw new Error("!avatar");
}
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)
const selector = (scheme === "erc721") ? "0xc87b56dd" : "0x0e89341c";
linkage.push({ type: scheme, value: avatar });
// The owner of this name
const owner = await this.getAddress();
if (owner == null) {
linkage.push({ type: "!owner", value: "" });
throw new Error("!owner");
}
const comps = (match[2] || "").split("/");
if (comps.length !== 2) {
linkage.push({ type: `!${scheme}caip`, value: (match[2] || "") });
throw new Error("!caip");
}
const addr = (0, index_js_1.getAddress)(comps[0]);
const tokenId = numPad(comps[1]);
// Check that this account owns the token
if (scheme === "erc721") {
// ownerOf(uint256 tokenId)
const tokenOwner = callAddress(await this.provider.call({
to: addr, data: (0, index_js_4.concat)(["0x6352211e", tokenId])
}));
if (owner !== tokenOwner) {
linkage.push({ type: "!owner", value: tokenOwner });
throw new Error("!owner");
}
linkage.push({ type: "owner", value: tokenOwner });
}
else if (scheme === "erc1155") {
// balanceOf(address owner, uint256 tokenId)
const balance = (0, index_js_4.getBigInt)(await this.provider.call({
to: addr, data: (0, index_js_4.concat)(["0x00fdd58e", (0, index_js_4.zeroPadValue)(owner, 32), tokenId])
}));
if (!balance) {
linkage.push({ type: "!balance", value: "0" });
throw new Error("!balance");
}
linkage.push({ type: "balance", value: balance.toString() });
}
// Call the token contract for the metadata URL
const tx = {
to: comps[0],
data: (0, index_js_4.concat)([selector, tokenId])
};
let metadataUrl = parseString(await this.provider.call(tx), 0);
if (metadataUrl == null) {
linkage.push({ type: "!metadata-url", value: "" });
throw new Error("!metadata-url");
}
linkage.push({ type: "metadata-url-base", value: metadataUrl });
// ERC-1155 allows a generic {id} in the URL
if (scheme === "erc1155") {
metadataUrl = metadataUrl.replace("{id}", (0, index_js_4.hexlify)(tokenId).substring(2));
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 = {};
const response = await (new index_js_4.FetchRequest(metadataUrl)).send();
response.assertOk();
try {
metadata = response.bodyJson;
}
catch (error) {
try {
linkage.push({ type: "!metadata", value: response.bodyText });
}
catch (error) {
const bytes = response.body;
if (bytes) {
linkage.push({ type: "!metadata", value: (0, index_js_4.hexlify)(bytes) });
}
throw error;
}
throw error;
}
if (!metadata) {
linkage.push({ type: "!metadata", value: "" });
throw new Error("!metadata");
}
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: "" });
throw new Error("!imageUrl");
}
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 });
throw new Error("!imageUrl-ipfs");
}
linkage.push({ type: "imageUrl-ipfs", value: imageUrl });
imageUrl = getIpfsLink(imageUrl);
}
linkage.push({ type: "url", value: imageUrl });
return { linkage, url: imageUrl };
}
}
}
}
catch (error) {
console.log("EE", error);
}
return { linkage, url: null };
}
static async #getResolver(provider, name) {
const network = await provider.getNetwork();
const ensPlugin = network.getPlugin("org.ethers.network-plugins.ens");
// No ENS...
(0, index_js_4.assert)(ensPlugin, "network does not support ENS", "UNSUPPORTED_OPERATION", {
operation: "getResolver", info: { network: network.name }
});
try {
// keccak256("resolver(bytes32)")
const addrData = await provider.call({
to: ensPlugin.address,
data: (0, index_js_4.concat)(["0x0178b8bf", (0, index_js_3.namehash)(name)]),
enableCcipRead: true
});
const addr = callAddress(addrData);
if (addr === (0, index_js_4.dataSlice)(index_js_2.ZeroHash, 0, 20)) {
return null;
}
return addr;
}
catch (error) {
// ENS registry cannot throw errors on resolver(bytes32),
// so probably a link error
throw error;
}
return null;
}
/**
* Resolve to the ENS resolver for %%name%% using %%provider%% or
* ``null`` if uncinfigured.
*/
static async fromName(provider, name) {
let currentName = name;
while (true) {
if (currentName === "" || currentName === ".") {
return null;
}
// Optimization since the eth node cannot change and does
// not have a wildcar resolver
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