2
0
mirror of synced 2025-02-23 11:38:42 +00:00

Added basic ENS resolver functions for contenthash, text and multi-coin addresses (#1003).

This commit is contained in:
Richard Moore 2020-09-04 01:18:57 -04:00
parent f24240eddf
commit 83db8a6bd1
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651
3 changed files with 261 additions and 14 deletions

View File

@ -19,6 +19,7 @@
"@ethersproject/abstract-provider": "^5.0.3",
"@ethersproject/abstract-signer": "^5.0.3",
"@ethersproject/address": "^5.0.3",
"@ethersproject/basex": "^5.0.3",
"@ethersproject/bignumber": "^5.0.6",
"@ethersproject/bytes": "^5.0.4",
"@ethersproject/constants": "^5.0.3",
@ -28,9 +29,11 @@
"@ethersproject/properties": "^5.0.3",
"@ethersproject/random": "^5.0.3",
"@ethersproject/rlp": "^5.0.3",
"@ethersproject/sha2": "^5.0.3",
"@ethersproject/strings": "^5.0.3",
"@ethersproject/transactions": "^5.0.3",
"@ethersproject/web": "^5.0.4",
"bech32": "1.1.4",
"ws": "7.2.3"
},
"description": "Ethereum Providers for ethers.",

View File

@ -4,15 +4,20 @@ import {
Block, BlockTag, BlockWithTransactions, EventType, Filter, FilterByBlockHash, ForkEvent,
Listener, Log, Provider, TransactionReceipt, TransactionRequest, TransactionResponse
} from "@ethersproject/abstract-provider";
import { Base58 } from "@ethersproject/basex";
import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
import { arrayify, hexDataLength, hexlify, hexValue, isHexString } from "@ethersproject/bytes";
import { arrayify, concat, hexConcat, hexDataLength, hexDataSlice, hexlify, hexValue, hexZeroPad, isHexString } from "@ethersproject/bytes";
import { HashZero } from "@ethersproject/constants";
import { namehash } from "@ethersproject/hash";
import { getNetwork, Network, Networkish } from "@ethersproject/networks";
import { Deferrable, defineReadOnly, getStatic, resolveProperties } from "@ethersproject/properties";
import { Transaction } from "@ethersproject/transactions";
import { toUtf8String } from "@ethersproject/strings";
import { sha256 } from "@ethersproject/sha2";
import { toUtf8Bytes, toUtf8String } from "@ethersproject/strings";
import { poll } from "@ethersproject/web";
import { encode, toWords } from "bech32";
import { Logger } from "@ethersproject/logger";
import { version } from "./_version";
const logger = new Logger(version);
@ -179,12 +184,246 @@ export class Event {
}
}
export interface EnsResolver {
// Name this Resolver is associated with
readonly name: string;
// The address of the resolver
readonly address: string;
// Multichain address resolution (also normal address resolution)
// See: https://eips.ethereum.org/EIPS/eip-2304
getAddress(coinType?: 60): Promise<string>
// Contenthash field
// See: https://eips.ethereum.org/EIPS/eip-1577
getContentHash(): Promise<string>;
// Storage of text records
// See: https://eips.ethereum.org/EIPS/eip-634
getText(key: string): Promise<string>;
};
export interface EnsProvider {
resolveName(name: string): Promise<string>;
lookupAddress(address: string): Promise<string>;
getResolver(name: string): Promise<EnsResolver>;
}
type CoinInfo = {
symbol: string,
ilk?: string, // General family
prefix?: string, // Bech32 prefix
p2pkh?: number, // Pay-to-Public-Key-Hash Version
p2sh?: number, // Pay-to-Script-Hash Version
};
// https://github.com/satoshilabs/slips/blob/master/slip-0044.md
const coinInfos: { [ coinType: string ]: CoinInfo } = {
"0": { symbol: "btc", p2pkh: 0x00, p2sh: 0x05, prefix: "bc" },
"2": { symbol: "ltc", p2pkh: 0x30, p2sh: 0x32, prefix: "ltc" },
"3": { symbol: "doge", p2pkh: 0x1e, p2sh: 0x16 },
"60": { symbol: "eth", ilk: "eth" },
"61": { symbol: "etc", ilk: "eth" },
"700": { symbol: "xdai", ilk: "eth" },
};
function bytes32ify(value: number): string {
return hexZeroPad(BigNumber.from(value).toHexString(), 32);
}
// Compute the Base58Check encoded data (checksum is first 4 bytes of sha256d)
function base58Encode(data: Uint8Array): string {
return Base58.encode(concat([ data, hexDataSlice(sha256(sha256(data)), 0, 4) ]));
}
export class Resolver implements EnsResolver {
readonly provider: BaseProvider;
readonly name: string;
readonly address: string;
constructor(provider: BaseProvider, address: string, name: string) {
defineReadOnly(this, "provider", provider);
defineReadOnly(this, "name", name);
defineReadOnly(this, "address", provider.formatter.address(address));
}
async _fetchBytes(selector: string, parameters?: string): Promise<string> {
// keccak256("addr(bytes32,uint256)")
const transaction = {
to: this.address,
data: hexConcat([ selector, namehash(this.name), (parameters || "0x") ])
};
const result = await this.provider.call(transaction);
if (result === "0x") { return null; }
const offset = BigNumber.from(hexDataSlice(result, 0, 32)).toNumber();
const length = BigNumber.from(hexDataSlice(result, offset, offset + 32)).toNumber();
return hexDataSlice(result, offset + 32, offset + 32 + length);
}
_getAddress(coinType: number, hexBytes: string): string {
const coinInfo = coinInfos[String(coinType)];
if (coinInfo == null) {
logger.throwError(`unsupported coin type: ${ coinType }`, Logger.errors.UNSUPPORTED_OPERATION, {
operation: `getAddress(${ coinType })`
});
}
if (coinInfo.ilk === "eth") {
return this.provider.formatter.address(hexBytes);
}
const bytes = arrayify(hexBytes);
// P2PKH: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
if (coinInfo.p2pkh != null) {
const p2pkh = hexBytes.match(/^0x76a9([0-9a-f][0-9a-f])([0-9a-f]*)88ac$/);
if (p2pkh) {
const length = parseInt(p2pkh[1], 16);
if (p2pkh[2].length === length * 2 && length >= 1 && length <= 75) {
return base58Encode(concat([ [ coinInfo.p2pkh ], ("0x" + p2pkh[2]) ]));
}
}
}
// P2SH: OP_HASH160 <scriptHash> OP_EQUAL
if (coinInfo.p2sh != null) {
const p2sh = hexBytes.match(/^0xa9([0-9a-f][0-9a-f])([0-9a-f]*)87$/);
if (p2sh) {
const length = parseInt(p2sh[1], 16);
if (p2sh[2].length === length * 2 && length >= 1 && length <= 75) {
return base58Encode(concat([ [ coinInfo.p2sh ], ("0x" + p2sh[2]) ]));
}
}
}
// Bech32
if (coinInfo.prefix != null) {
const length = bytes[1];
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#witness-program
let version = bytes[0];
if (version === 0x00) {
if (length !== 20 && length !== 32) {
version = -1;
}
} else {
version = -1;
}
if (version >= 0 && bytes.length === 2 + length && length >= 1 && length <= 75) {
const words = toWords(bytes.slice(2));
words.unshift(version);
return encode(coinInfo.prefix, words);
}
}
return null;
}
async getAddress(coinType?: number): Promise<string> {
if (coinType == null) { coinType = 60; }
// If Ethereum, use the standard `addr(bytes32)`
if (coinType === 60) {
// keccak256("addr(bytes32)")
const transaction = {
to: this.address,
data: ("0x3b3b57de" + namehash(this.name).substring(2))
};
const hexBytes = await this.provider.call(transaction);
// No address
if (hexBytes === "0x" || hexBytes === HashZero) { return null; }
return this.provider.formatter.callAddress(hexBytes);
}
// keccak256("addr(bytes32,uint256")
const hexBytes = await this._fetchBytes("0xf1cb7e06", bytes32ify(coinType));
// No address
if (hexBytes == null || hexBytes === "0x") { return null; }
// Compute the address
const address = this._getAddress(coinType, hexBytes);
if (address == null) {
logger.throwError(`invalid or unsupported coin data`, Logger.errors.UNSUPPORTED_OPERATION, {
operation: `getAddress(${ coinType })`,
coinType: coinType,
data: hexBytes
});
}
return address;
}
async getContentHash(): Promise<string> {
// keccak256("contenthash()")
const hexBytes = await this._fetchBytes("0xbc1c58d1");
// No contenthash
if (hexBytes == null || hexBytes === "0x") { return null; }
// IPFS (CID: 1, Type: DAG-PB)
const ipfs = hexBytes.match(/^0xe3010170(([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f]*))$/);
if (ipfs) {
const length = parseInt(ipfs[3], 16);
if (ipfs[4].length === length * 2) {
return "ipfs:/\/" + Base58.encode("0x" + ipfs[1]);
}
}
// Swarm (CID: 1, Type: swarm-manifest; hash/length hard-coded to keccak256/32)
const swarm = hexBytes.match(/^0xe40101fa011b20([0-9a-f]*)$/)
if (swarm) {
if (swarm[1].length === (32 * 2)) {
return "bzz:/\/" + swarm[1]
}
}
return logger.throwError(`invalid or unsupported content hash data`, Logger.errors.UNSUPPORTED_OPERATION, {
operation: "getContentHash()",
data: hexBytes
});
}
async getText(key: string): Promise<string> {
// The key encoded as parameter to fetchBytes
let keyBytes = 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
keyBytes = concat([ bytes32ify(64), bytes32ify(keyBytes.length), keyBytes ]);
// Pad to word-size (32 bytes)
if ((keyBytes.length % 32) !== 0) {
keyBytes = concat([ keyBytes, hexZeroPad("0x", 32 - (key.length % 32)) ])
}
const hexBytes = await this._fetchBytes("0x59d1d43c", hexlify(keyBytes));
if (hexBytes == null || hexBytes === "0x") { return null; }
return toUtf8String(hexBytes);
}
}
let defaultFormatter: Formatter = null;
let nextPollId = 1;
export class BaseProvider extends Provider {
export class BaseProvider extends Provider implements EnsProvider {
_networkPromise: Promise<Network>;
_network: Network;
@ -1046,6 +1285,12 @@ export class BaseProvider extends Provider {
}
async getResolver(name: string): Promise<Resolver> {
const address = await this._getResolver(name);
if (address == null) { return null; }
return new Resolver(this, address, name);
}
async _getResolver(name: string): Promise<string> {
// Get the resolver from the blockchain
const network = await this.getNetwork();
@ -1084,16 +1329,10 @@ export class BaseProvider extends Provider {
}
// Get the addr from the resovler
const resolverAddress = await this._getResolver(name);
if (!resolverAddress) { return null; }
const resolver = await this.getResolver(name);
if (!resolver) { return null; }
// keccak256("addr(bytes32)")
const transaction = {
to: resolverAddress,
data: ("0x3b3b57de" + namehash(name).substring(2))
};
return this.formatter.callAddress(await this.call(transaction));
return await resolver.getAddress();
}
async lookupAddress(address: string | Promise<string>): Promise<string> {

View File

@ -16,7 +16,7 @@ import {
import { getNetwork } from "@ethersproject/networks";
import { Network, Networkish } from "@ethersproject/networks";
import { BaseProvider } from "./base-provider";
import { BaseProvider, EnsProvider, EnsResolver, Resolver } from "./base-provider";
import { AlchemyProvider } from "./alchemy-provider";
import { CloudflareProvider } from "./cloudflare-provider";
@ -93,6 +93,8 @@ export {
Provider,
BaseProvider,
Resolver,
UrlJsonRpcProvider,
///////////////////////
@ -149,6 +151,9 @@ export {
JsonRpcFetchFunc,
Network,
Networkish
Networkish,
EnsProvider,
EnsResolver
};