From 03152ea0145de17912c035874b180125b7f353cf Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Mon, 24 Jan 2022 16:32:41 -0500 Subject: [PATCH 1/2] Include trouble-shooting URLs in errors (#2489). --- packages/bignumber/src.ts/bignumber.ts | 18 ++++++------ packages/logger/src.ts/index.ts | 39 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/packages/bignumber/src.ts/bignumber.ts b/packages/bignumber/src.ts/bignumber.ts index 656018909d..8726618235 100644 --- a/packages/bignumber/src.ts/bignumber.ts +++ b/packages/bignumber/src.ts/bignumber.ts @@ -83,7 +83,7 @@ export class BigNumber implements Hexable { div(other: BigNumberish): BigNumber { const o = BigNumber.from(other); if (o.isZero()) { - throwFault("division by zero", "div"); + throwFault("division-by-zero", "div"); } return toBigNumber(toBN(this).div(toBN(other))); } @@ -95,7 +95,7 @@ export class BigNumber implements Hexable { mod(other: BigNumberish): BigNumber { const value = toBN(other); if (value.isNeg()) { - throwFault("cannot modulo negative values", "mod"); + throwFault("division-by-zero", "mod"); } return toBigNumber(toBN(this).umod(value)); } @@ -103,7 +103,7 @@ export class BigNumber implements Hexable { pow(other: BigNumberish): BigNumber { const value = toBN(other); if (value.isNeg()) { - throwFault("cannot raise to negative values", "pow"); + throwFault("negative-power", "pow"); } return toBigNumber(toBN(this).pow(value)); } @@ -111,7 +111,7 @@ export class BigNumber implements Hexable { and(other: BigNumberish): BigNumber { const value = toBN(other); if (this.isNegative() || value.isNeg()) { - throwFault("cannot 'and' negative values", "and"); + throwFault("unbound-bitwise-result", "and"); } return toBigNumber(toBN(this).and(value)); } @@ -119,7 +119,7 @@ export class BigNumber implements Hexable { or(other: BigNumberish): BigNumber { const value = toBN(other); if (this.isNegative() || value.isNeg()) { - throwFault("cannot 'or' negative values", "or"); + throwFault("unbound-bitwise-result", "or"); } return toBigNumber(toBN(this).or(value)); } @@ -127,28 +127,28 @@ export class BigNumber implements Hexable { xor(other: BigNumberish): BigNumber { const value = toBN(other); if (this.isNegative() || value.isNeg()) { - throwFault("cannot 'xor' negative values", "xor"); + throwFault("unbound-bitwise-result", "xor"); } return toBigNumber(toBN(this).xor(value)); } mask(value: number): BigNumber { if (this.isNegative() || value < 0) { - throwFault("cannot mask negative values", "mask"); + throwFault("negative-width", "mask"); } return toBigNumber(toBN(this).maskn(value)); } shl(value: number): BigNumber { if (this.isNegative() || value < 0) { - throwFault("cannot shift negative values", "shl"); + throwFault("negative-width", "shl"); } return toBigNumber(toBN(this).shln(value)); } shr(value: number): BigNumber { if (this.isNegative() || value < 0) { - throwFault("cannot shift negative values", "shr"); + throwFault("negative-width", "shr"); } return toBigNumber(toBN(this).shrn(value)); } diff --git a/packages/logger/src.ts/index.ts b/packages/logger/src.ts/index.ts index 87808ec611..c568f034f3 100644 --- a/packages/logger/src.ts/index.ts +++ b/packages/logger/src.ts/index.ts @@ -217,6 +217,45 @@ export class Logger { messageDetails.push(`version=${ this.version }`); const reason = message; + + let url = ""; + + switch (code) { + case ErrorCode.NUMERIC_FAULT: { + url = "NUMERIC_FAULT"; + const fault = message; + + switch (fault) { + case "overflow": case "underflow": + url += "-" + fault; + break; + case "division-by-zero": case "negative-modulo": + url += "-undefined"; + break; + case "negative-power": case "negative-width": + url += "-unsupported"; + break; + case "unbound-bitwise-result": + url += "-unbound-result"; + break; + } + break; + } + case ErrorCode.CALL_EXCEPTION: + case ErrorCode.INSUFFICIENT_FUNDS: + case ErrorCode.MISSING_NEW: + case ErrorCode.NONCE_EXPIRED: + case ErrorCode.REPLACEMENT_UNDERPRICED: + case ErrorCode.TRANSACTION_REPLACED: + case ErrorCode.UNPREDICTABLE_GAS_LIMIT: + url = code; + break; + } + + if (url) { + message += " [ See: https:/\/ethers.org/errors/" + url + " ]"; + } + if (messageDetails.length) { message += " (" + messageDetails.join(", ") + ")"; } From 83891f9258492f9801aa579ac50764b6491b6180 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Fri, 18 Feb 2022 18:15:03 -0500 Subject: [PATCH 2/2] Add EIP-2544 Wildcard support (#2477, #2582, #2583). --- packages/ethers/src.ts/utils.ts | 3 +- packages/hash/src.ts/index.ts | 3 +- packages/hash/src.ts/namehash.ts | 9 + packages/providers/src.ts/base-provider.ts | 232 +++++++++++++++------ 4 files changed, 187 insertions(+), 60 deletions(-) diff --git a/packages/ethers/src.ts/utils.ts b/packages/ethers/src.ts/utils.ts index 7a4de871ad..e588882609 100644 --- a/packages/ethers/src.ts/utils.ts +++ b/packages/ethers/src.ts/utils.ts @@ -5,7 +5,7 @@ import { getAddress, getCreate2Address, getContractAddress, getIcapAddress, isAd import * as base64 from "@ethersproject/base64"; import { Base58 as base58 } from "@ethersproject/basex"; import { arrayify, concat, hexConcat, hexDataSlice, hexDataLength, hexlify, hexStripZeros, hexValue, hexZeroPad, isBytes, isBytesLike, isHexString, joinSignature, zeroPad, splitSignature, stripZeros } from "@ethersproject/bytes"; -import { _TypedDataEncoder, hashMessage, id, isValidName, namehash } from "@ethersproject/hash"; +import { _TypedDataEncoder, dnsEncode, hashMessage, id, isValidName, namehash } from "@ethersproject/hash"; import { defaultPath, entropyToMnemonic, getAccountPath, HDNode, isValidMnemonic, mnemonicToEntropy, mnemonicToSeed } from "@ethersproject/hdnode"; import { getJsonWalletAddress } from "@ethersproject/json-wallets"; import { keccak256 } from "@ethersproject/keccak256"; @@ -114,6 +114,7 @@ export { formatBytes32String, parseBytes32String, + dnsEncode, hashMessage, namehash, isValidName, diff --git a/packages/hash/src.ts/index.ts b/packages/hash/src.ts/index.ts index ec420e5746..908314779d 100644 --- a/packages/hash/src.ts/index.ts +++ b/packages/hash/src.ts/index.ts @@ -1,7 +1,7 @@ "use strict"; import { id } from "./id"; -import { isValidName, namehash } from "./namehash"; +import { dnsEncode, isValidName, namehash } from "./namehash"; import { hashMessage, messagePrefix } from "./message"; import { TypedDataEncoder as _TypedDataEncoder } from "./typed-data"; @@ -9,6 +9,7 @@ import { TypedDataEncoder as _TypedDataEncoder } from "./typed-data"; export { id, + dnsEncode, namehash, isValidName, diff --git a/packages/hash/src.ts/namehash.ts b/packages/hash/src.ts/namehash.ts index 856a0be5a8..6c119221f3 100644 --- a/packages/hash/src.ts/namehash.ts +++ b/packages/hash/src.ts/namehash.ts @@ -46,3 +46,12 @@ export function namehash(name: string): string { return hexlify(result); } +export function dnsEncode(name: string): string { + return hexlify(concat(name.split(".").map((comp) => { + // We jam in an _ prefix to fill in with the length later + // Note: Nameprep throws if the component is over 63 bytes + const bytes = toUtf8Bytes("_" + nameprep(comp)); + bytes[0] = bytes.length - 1; + return bytes; + }))) + "00"; +} diff --git a/packages/providers/src.ts/base-provider.ts b/packages/providers/src.ts/base-provider.ts index 299b2ab6cd..f7c753738f 100644 --- a/packages/providers/src.ts/base-provider.ts +++ b/packages/providers/src.ts/base-provider.ts @@ -8,7 +8,7 @@ import { Base58 } from "@ethersproject/basex"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; import { arrayify, concat, hexConcat, hexDataLength, hexDataSlice, hexlify, hexValue, hexZeroPad, isHexString } from "@ethersproject/bytes"; import { HashZero } from "@ethersproject/constants"; -import { namehash } from "@ethersproject/hash"; +import { dnsEncode, namehash } from "@ethersproject/hash"; import { getNetwork, Network, Networkish } from "@ethersproject/networks"; import { Deferrable, defineReadOnly, getStatic, resolveProperties } from "@ethersproject/properties"; import { Transaction } from "@ethersproject/transactions"; @@ -267,9 +267,34 @@ function _parseBytes(result: string): null | string { // Trim off the ipfs:// prefix and return the default gateway URL function getIpfsLink(link: string): string { - return `https:/\/gateway.ipfs.io/ipfs/${ link.substring(7) }`; + if (link.match(/^ipfs:\/\/ipfs\//i)) { + link = link.substring(12); + } else if (link.match(/^ipfs:\/\//i)) { + link = link.substring(7); + } else { + logger.throwArgumentError("unsupported IPFS format", "link", link); + } + return `https:/\/gateway.ipfs.io/ipfs/${ link }`; } +function numPad(value: number): Uint8Array { + const result = arrayify(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: Uint8Array): Uint8Array { + if ((value.length % 32) === 0) { return value; } + + const result = new Uint8Array(Math.ceil(value.length / 32) * 32); + result.set(value); + return result; +} + + export class Resolver implements EnsResolver { readonly provider: BaseProvider; @@ -278,6 +303,9 @@ export class Resolver implements EnsResolver { readonly _resolvedAddress: null | string; + // For EIP-2544 names, the ancestor that provided the resolver + _supportsEip2544: null | Promise; + // The resolvedAddress is only for creating a ReverseLookup resolver constructor(provider: BaseProvider, address: string, name: string, resolvedAddress?: string) { defineReadOnly(this, "provider", provider); @@ -286,21 +314,85 @@ export class Resolver implements EnsResolver { defineReadOnly(this, "_resolvedAddress", resolvedAddress); } - async _fetchBytes(selector: string, parameters?: string): Promise { + supportsWildcard(): Promise { + if (!this._supportsEip2544) { + // supportsInterface(bytes4 = selector("resolve(bytes,bytes)")) + this._supportsEip2544 = this.provider.call({ + to: this.address, + data: "0x01ffc9a79061b92300000000000000000000000000000000000000000000000000000000" + }).then((result) => { + return BigNumber.from(result).eq(1); + }).catch((error) => { + if (error.code === Logger.errors.CALL_EXCEPTION) { return false; } + // Rethrow the error: link is down, etc. Let future attempts retry. + this._supportsEip2544 = null; + throw error; + }); + } + + return this._supportsEip2544; + } + + async _fetch(selector: string, parameters?: string): Promise { // e.g. keccak256("addr(bytes32,uint256)") const tx = { to: this.address, data: hexConcat([ selector, namehash(this.name), (parameters || "0x") ]) }; + // Wildcard support; use EIP-2544 to resolve the request + let parseBytes = false; + if (await this.supportsWildcard()) { + parseBytes = true; + + const p0 = arrayify(dnsEncode(this.name)); + const p1 = arrayify(tx.data); + + // selector("resolve(bytes,bytes)") + const bytes: Array = [ "0x9061b923" ]; + let byteCount = 0; + + // Place-holder pointer to p0 + const placeHolder0 = bytes.length; + bytes.push("0x"); + byteCount += 32; + + // Place-holder pointer to p1 + const placeHolder1 = bytes.length; + bytes.push("0x"); + byteCount += 32; + + // The length and padded value of p0 + bytes[placeHolder0] = numPad(byteCount); + bytes.push(numPad(p0.length)); + bytes.push(bytesPad(p0)); + byteCount += 32 + Math.ceil(p0.length / 32) * 32; + + // The length and padded value of p0 + bytes[placeHolder1] = numPad(byteCount); + bytes.push(numPad(p1.length)); + bytes.push(bytesPad(p1)); + byteCount += 32 + Math.ceil(p1.length / 32) * 32; + + tx.data = hexConcat(bytes); + } + try { - return _parseBytes(await this.provider.call(tx)); + let result = await this.provider.call(tx); + if (parseBytes) { result = _parseBytes(result); } + return result; } catch (error) { if (error.code === Logger.errors.CALL_EXCEPTION) { return null; } return null; } } + async _fetchBytes(selector: string, parameters?: string): Promise { + const result = await this._fetch(selector, parameters); + if (result != null) { return _parseBytes(result); } + return null; + } + _getAddress(coinType: number, hexBytes: string): string { const coinInfo = coinInfos[String(coinType)]; @@ -370,16 +462,12 @@ export class Resolver implements EnsResolver { if (coinType === 60) { try { // keccak256("addr(bytes32)") - const transaction = { - to: this.address, - data: ("0x3b3b57de" + namehash(this.name).substring(2)) - }; - const hexBytes = await this.provider.call(transaction); + const result = await this._fetch("0x3b3b57de"); // No address - if (hexBytes === "0x" || hexBytes === HashZero) { return null; } + if (result === "0x" || result === HashZero) { return null; } - return this.provider.formatter.callAddress(hexBytes); + return this.provider.formatter.callAddress(result); } catch (error) { if (error.code === Logger.errors.CALL_EXCEPTION) { return null; } throw error; @@ -473,7 +561,7 @@ export class Resolver implements EnsResolver { }; let metadataUrl = _parseString(await this.provider.call(tx)) if (metadataUrl == null) { return null; } - linkage.push({ type: "metadata-url", content: metadataUrl }); + linkage.push({ type: "metadata-url-base", content: metadataUrl }); // ERC-1155 allows a generic {id} in the URL if (scheme === "erc1155") { @@ -481,6 +569,13 @@ export class Resolver implements EnsResolver { linkage.push({ type: "metadata-url-expanded", content: metadataUrl }); } + // Transform IPFS metadata links + if (metadataUrl.match(/^ipfs:/i)) { + metadataUrl = getIpfsLink(metadataUrl); + } + + linkage.push({ type: "metadata-url", content: metadataUrl }); + // Get the token metadata const metadata = await fetchJson(metadataUrl); if (!metadata) { return null; } @@ -1656,18 +1751,36 @@ export class BaseProvider extends Provider implements EnsProvider { async getResolver(name: string): Promise { - try { - const address = await this._getResolver(name); - if (address == null) { return null; } - return new Resolver(this, address, name); - } catch (error) { - if (error.code === Logger.errors.CALL_EXCEPTION) { return null; } - throw error; + 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 this._getResolver(currentName, "getResolver"); + + // Found a resolver! + if (addr != null) { + const resolver = new Resolver(this, 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("."); } + } - async _getResolver(name: string): Promise { - // Get the resolver from the blockchain + async _getResolver(name: string, operation?: string): Promise { + if (operation == null) { operation = "ENS"; } + const network = await this.getNetwork(); // No ENS... @@ -1675,22 +1788,22 @@ export class BaseProvider extends Provider implements EnsProvider { logger.throwError( "network does not support ENS", Logger.errors.UNSUPPORTED_OPERATION, - { operation: "ENS", network: network.name } + { operation, network: network.name } ); } - // keccak256("resolver(bytes32)") - const transaction = { - to: network.ensAddress, - data: ("0x0178b8bf" + namehash(name).substring(2)) - }; - try { - return this.formatter.callAddress(await this.call(transaction)); + // keccak256("resolver(bytes32)") + const addrData = await this.call({ + to: network.ensAddress, + data: ("0x0178b8bf" + namehash(name).substring(2)) + }); + return this.formatter.callAddress(addrData); } catch (error) { - if (error.code === Logger.errors.CALL_EXCEPTION) { return null; } - throw error; + // ENS registry cannot throw errors on resolver(bytes32) } + + return null; } async resolveName(name: string | Promise): Promise { @@ -1719,34 +1832,17 @@ export class BaseProvider extends Provider implements EnsProvider { address = await address; address = this.formatter.address(address); - const reverseName = address.substring(2).toLowerCase() + ".addr.reverse"; + const node = address.substring(2).toLowerCase() + ".addr.reverse"; - const resolverAddress = await this._getResolver(reverseName); - if (!resolverAddress) { return null; } + const resolverAddr = await this._getResolver(node, "lookupAddress"); + if (resolverAddr == null) { return null; } // keccak("name(bytes32)") - let bytes = arrayify(await this.call({ - to: resolverAddress, - data: ("0x691f3431" + namehash(reverseName).substring(2)) + const name = _parseString(await this.call({ + to: resolverAddr, + data: ("0x691f3431" + namehash(node).substring(2)) })); - // Strip off the dynamic string pointer (0x20) - if (bytes.length < 32 || !BigNumber.from(bytes.slice(0, 32)).eq(32)) { return null; } - bytes = bytes.slice(32); - - // Not a length-prefixed string - if (bytes.length < 32) { return null; } - - // Get the length of the string (from the length-prefix) - const length = BigNumber.from(bytes.slice(0, 32)).toNumber(); - bytes = bytes.slice(32); - - // Length longer than available data - if (length > bytes.length) { return null; } - - const name = toUtf8String(bytes.slice(0, length)); - - // Make sure the reverse record matches the foward record const addr = await this.resolveName(name); if (addr != address) { return null; } @@ -1759,15 +1855,35 @@ export class BaseProvider extends Provider implements EnsProvider { // Address; reverse lookup const address = this.formatter.address(nameOrAddress); - const reverseName = address.substring(2).toLowerCase() + ".addr.reverse"; + const node = address.substring(2).toLowerCase() + ".addr.reverse"; - const resolverAddress = await this._getResolver(reverseName); + const resolverAddress = await this._getResolver(node, "getAvatar"); if (!resolverAddress) { return null; } - resolver = new Resolver(this, resolverAddress, "_", address); + // Try resolving the avatar against the addr.reverse resolver + resolver = new Resolver(this, resolverAddress, node); + try { + const avatar = await resolver.getAvatar(); + if (avatar) { return avatar.url; } + } catch (error) { + if (error.code !== Logger.errors.CALL_EXCEPTION) { throw error; } + } + + // Try getting the name and performing forward lookup; allowing wildcards + try { + // keccak("name(bytes32)") + const name = _parseString(await this.call({ + to: resolverAddress, + data: ("0x691f3431" + namehash(node).substring(2)) + })); + resolver = await this.getResolver(name); + } catch (error) { + if (error.code !== Logger.errors.CALL_EXCEPTION) { throw error; } + return null; + } } else { - // ENS name; forward lookup + // ENS name; forward lookup with wildcard resolver = await this.getResolver(nameOrAddress); if (!resolver) { return null; } }