Skip to content
This repository has been archived by the owner on May 19, 2023. It is now read-only.

Commit

Permalink
Multichain resolver (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
javiesses authored Feb 3, 2020
1 parent 59659da commit 0b7f642
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 210 deletions.
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
// contracts interfaces
export const ADDR_INTERFACE = '0x3b3b57de';
export const ERC165_INTERFACE = '0x01ffc9a7';
export const CHAIN_ADDR_INTERFACE = '0x8be4b5f6';
2 changes: 2 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export const NO_ADDR_RESOLUTION = 'KB002: No addr resolution';
export const NO_RESOLVER = 'KB003: No resolver';
export const LIBRARY_NOT_COMPOSED = 'KB004: Library not composed';
export const NO_ADDRESSES_PROVIDED = 'KB005: No contract addresses provided';
export const NO_CHAIN_ADDR_RESOLUTION = 'KB006: No chain address resolution';
export const NO_CHAIN_ADDR_RESOLUTION_SET = 'KB007: No chain address resolution set';
3 changes: 3 additions & 0 deletions src/factories/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import RNSRegistryData from '@rsksmart/rns-registry/RNSRegistryData.json';
import AddrResolverData from '@rsksmart/rns-resolver/AddrResolverData.json';
import ChainAddrResolverData from '@rsksmart/rns-resolver/ChainAddrResolverData.json';
import Web3 from 'web3';
import { AbiItem } from 'web3-utils';
import { NetworkId } from '../types';
Expand All @@ -22,3 +23,5 @@ export const createContractAddresses = (networkId: NetworkId) => {
export const createRegistry = (web3: Web3, address: string) => new web3.eth.Contract(RNSRegistryData.abi as AbiItem[], address)

export const createAddrResolver = (web3: Web3, address: string) => new web3.eth.Contract(AddrResolverData.abi as AbiItem[], address)

export const createChainAddrResolver = (web3: Web3, address: string) => new web3.eth.Contract(ChainAddrResolverData.abi as AbiItem[], address)
61 changes: 19 additions & 42 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import Web3 from 'web3/types';
import { Contract } from 'web3-eth-contract/types';
import { RNS, Contracts, Options, ContractAddresses } from './types';
import { hash as namehash } from 'eth-ens-namehash';
import { createRegistry, createAddrResolver, createContractAddresses } from './factories';
import { NO_ADDR_RESOLUTION, NO_ADDR_RESOLUTION_SET, NO_RESOLVER, LIBRARY_NOT_COMPOSED } from './errors';
import { ZERO_ADDRESS, ADDR_INTERFACE, ERC165_INTERFACE } from './constants';
import { RNS, Contracts, Options, ContractAddresses, ChainId } from './types';
import { createRegistry, createContractAddresses } from './factories';
import { LIBRARY_NOT_COMPOSED } from './errors';
import Resolutions from './resolutions';

export default class implements RNS {
private _contracts!: Contracts
private _contractAddresses!: ContractAddresses
private _contracts!: Contracts;
private _contractAddresses!: ContractAddresses;
private _resolutionHelper!: Resolutions;
private _composed!: boolean;

constructor (public web3: Web3, options?: Options) {
if(options && options.contractAddresses) {
Expand All @@ -24,7 +24,11 @@ export default class implements RNS {
}

public async compose(): Promise<void> {
await this._detectNetwork()
if (!this._composed) {
await this._detectNetwork();
this._resolutionHelper = new Resolutions(this.web3, this._contracts.registry);
this._composed = true;
}
}

private async _detectNetwork() {
Expand All @@ -40,39 +44,12 @@ export default class implements RNS {
}
}

private async _hasMethod(contractAddress: string, signatureHash: string) {
const code = await this.web3.eth.getCode(contractAddress);
return code.indexOf(signatureHash.slice(2, signatureHash.length)) > 0;
}

async addr(domain: string): Promise<string> {
await this._detectNetwork();

const node: string = namehash(domain);

const resolverAddress: string = await this._contracts.registry.methods.resolver(node).call();

if (resolverAddress === ZERO_ADDRESS) {
throw new Error(NO_RESOLVER);
}
const isErc165Contract = await this._hasMethod(resolverAddress, ERC165_INTERFACE);
if (!isErc165Contract) {
throw new Error(NO_ADDR_RESOLUTION);
async addr(domain: string, chainId?: ChainId): Promise<string> {
await this.compose();
if (!chainId) {
return this._resolutionHelper.addr(domain);
} else {
return this._resolutionHelper.chainAddr(domain, chainId);
}

const addrResolver: Contract = createAddrResolver(this.web3, resolverAddress);

const supportsAddr: boolean = await addrResolver.methods.supportsInterface(ADDR_INTERFACE).call();
if (!supportsAddr) {
throw new Error(NO_ADDR_RESOLUTION);
}

const addr: string = await addrResolver.methods.addr(node).call();

if (addr === ZERO_ADDRESS){
throw new Error(NO_ADDR_RESOLUTION_SET);
}

return addr;
}
}
69 changes: 69 additions & 0 deletions src/resolutions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Web3 from 'web3/types';
import { Contract } from 'web3-eth-contract/types';
import { hash as namehash } from 'eth-ens-namehash';
import { hasMethod } from './utils';
import { createAddrResolver, createChainAddrResolver } from './factories';
import {
NO_ADDR_RESOLUTION, NO_ADDR_RESOLUTION_SET, NO_RESOLVER,
NO_CHAIN_ADDR_RESOLUTION, NO_CHAIN_ADDR_RESOLUTION_SET
} from './errors';
import { ZERO_ADDRESS, ADDR_INTERFACE, ERC165_INTERFACE, CHAIN_ADDR_INTERFACE } from './constants';
import { ChainId, Resolutions } from './types';

export default class implements Resolutions {
constructor(private web3: Web3, private registry: Contract) { }

private async _createResolver(
node: string,
methodInterface: string,
errorMessage: string,
contractFactory: (web3: Web3, address: string) => Contract
): Promise<Contract> {
const resolverAddress: string = await this.registry.methods.resolver(node).call();

if (resolverAddress === ZERO_ADDRESS) {
throw new Error(NO_RESOLVER);
}

const isErc165Contract = await hasMethod(this.web3, resolverAddress, ERC165_INTERFACE);
if (!isErc165Contract) {
throw new Error(errorMessage);
}

const resolver: Contract = contractFactory(this.web3, resolverAddress);

const supportsInterface: boolean = await resolver.methods.supportsInterface(methodInterface).call();
if (!supportsInterface) {
throw new Error(errorMessage);
}

return resolver;
}

async addr(domain: string): Promise<string> {
const node: string = namehash(domain);

const resolver = await this._createResolver(node, ADDR_INTERFACE, NO_ADDR_RESOLUTION, createAddrResolver);

const addr: string = await resolver.methods.addr(node).call();

if (addr === ZERO_ADDRESS){
throw new Error(NO_ADDR_RESOLUTION_SET);
}

return addr;
}

async chainAddr(domain: string, chainId: ChainId) {
const node: string = namehash(domain);

const resolver = await this._createResolver(node, CHAIN_ADDR_INTERFACE, NO_CHAIN_ADDR_RESOLUTION, createChainAddrResolver);

const addr: string = await resolver.methods.chainAddr(node, chainId).call();
if (!addr || addr === ZERO_ADDRESS){
throw new Error(NO_CHAIN_ADDR_RESOLUTION_SET);
}

return addr;
}
}
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ export enum NetworkId {
RSK_TESTNET = 31
}

export enum ChainId {
RSK_MAINNET = '0x80000089',
BITCOIN_MAINNET = '0x80000000',
ETHEREUM_MAINNET = '0x8000003c',
LITECOIN = '0x80000002'
}

export interface ContractAddresses {
registry: string
}
Expand All @@ -22,5 +29,10 @@ export interface RNS {
web3: Web3
contracts: Contracts
compose(): void
addr(domain: string, chainId?: ChainId): Promise<string>
}

export interface Resolutions {
addr(domain: string): Promise<string>
chainAddr(domain: string, chainId: ChainId): Promise<string>
}
6 changes: 6 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Web3 from "web3";

export const hasMethod = async(web3: Web3, contractAddress: string, signatureHash: string) => {
const code = await web3.eth.getCode(contractAddress);
return code.indexOf(signatureHash.slice(2, signatureHash.length)) > 0;
}
98 changes: 98 additions & 0 deletions test/rns.addr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import Web3 from 'web3';
import RNS from '../src/index';
import { NO_ADDR_RESOLUTION_SET, NO_RESOLVER, NO_ADDR_RESOLUTION } from '../src/errors';
import { asyncExpectThrowError } from './utils';

const PUBLIC_NODE_MAINNET = 'https://public-node.rsk.co';
const PUBLIC_NODE_TESTNET = 'https://public-node.testnet.rsk.co';

describe ('addr resolution', () => {
describe ('should resolve a name', () => {
test('mainnet', async () => {
const web3 = new Web3(PUBLIC_NODE_MAINNET);
const rns = new RNS(web3);
const addr = await rns.addr('testing.rsk');
expect(addr).toBe('0x0000000000000000000000000000000001000006');
});

test('testnet', async () => {
const web3 = new Web3(PUBLIC_NODE_TESTNET);
const rns = new RNS(web3);
const addr = await rns.addr('testing.rsk');
expect(addr).toBe('0x0000000000000000000000000000000001000006');
});
});

describe ('should throw an error when resolver has not been set', () => {
test('mainnet', async () => {
const web3 = new Web3(PUBLIC_NODE_MAINNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('noresolver.testing.rsk'), NO_RESOLVER);
});

test('testnet', async () => {
const web3 = new Web3(PUBLIC_NODE_TESTNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('noresolver.testing.rsk'), NO_RESOLVER);
});
});

describe ('should throw an error when resolver does not support addr interface', () => {
describe ('ERC165 contract as resolver that not implements addr method', () => {
// the resolver address is the NameResolver contract. Is an ERC165 that not supports addr interface
test('mainnet', async () => {
const web3 = new Web3(PUBLIC_NODE_MAINNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('noaddrresolver.testing.rsk'), NO_ADDR_RESOLUTION);
});

test('testnet with another ERC165 as resolver', async () => {
const web3 = new Web3(PUBLIC_NODE_TESTNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('noaddrresolver.testing.rsk'), NO_ADDR_RESOLUTION);
});
});

describe('non contract address as a resolver', () => {
test('mainnet', async () => {
const web3 = new Web3(PUBLIC_NODE_MAINNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('accountasresolver.testing.rsk'), NO_ADDR_RESOLUTION);
});

test('testnet', async () => {
const web3 = new Web3(PUBLIC_NODE_TESTNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('accountasresolver.testing.rsk'), NO_ADDR_RESOLUTION);
});
});
});

describe ('should throw an error when no resolution set', () => {
test('mainnet', async () => {
const web3 = new Web3(PUBLIC_NODE_MAINNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('noresolution.testing.rsk'), NO_ADDR_RESOLUTION_SET);
});

test('testnet', async () => {
const web3 = new Web3(PUBLIC_NODE_TESTNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('noresolution.testing.rsk'), NO_ADDR_RESOLUTION_SET);
});
});

describe ('should throw an error when domain do not exist', () => {
test('mainnet', async () => {
const web3 = new Web3(PUBLIC_NODE_MAINNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('noexist.testing.rsk'), NO_RESOLVER);
});

test('testnet', async () => {
const web3 = new Web3(PUBLIC_NODE_TESTNET);
const rns = new RNS(web3);
await asyncExpectThrowError(async () => await rns.addr('noexist.testing.rsk'), NO_RESOLVER);
});
});
});
Loading

0 comments on commit 0b7f642

Please sign in to comment.