Skip to content

Commit

Permalink
Added basic ENS resolver functions for contenthash, text and multi-co…
Browse files Browse the repository at this point in the history
…in addresses (#1003).
  • Loading branch information
ricmoo committed Sep 4, 2020
1 parent f24240e commit 83db8a6
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 14 deletions.
3 changes: 3 additions & 0 deletions packages/providers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.",
Expand Down
263 changes: 251 additions & 12 deletions packages/providers/src.ts/base-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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> {
Expand Down
9 changes: 7 additions & 2 deletions packages/providers/src.ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -93,6 +93,8 @@ export {
Provider,
BaseProvider,

Resolver,

UrlJsonRpcProvider,

///////////////////////
Expand Down Expand Up @@ -149,6 +151,9 @@ export {
JsonRpcFetchFunc,

Network,
Networkish
Networkish,

EnsProvider,
EnsResolver
};

0 comments on commit 83db8a6

Please sign in to comment.