ENS Resolution for AddressField Component (#807)

* Refactor BaseNode to be an interface INode

* Initial contract commit

* Remove redundant fallback ABI function

* First working iteration of Contract generator to be used in ENS branch

* Hide abi to clean up logging output

* Strip 0x prefix from output decode

* Handle unnamed output params

* Implement ability to supply output mappings to ABI functions

* Fix null case in outputMapping

* Add flow typing

* Add .call method to functions

* Partial commit for type refactor

* Temp contract type fix -- waiting for NPM modularization

* Remove empty files

* Cleanup contract

* Add call request to node interface

* Fix output mapping types

* Revert destructuring overboard

* Add sendCallRequest to rpcNode class and add typing

* Use enum for selecting ABI methods

* Add transaction capability to contracts

* Cleanup privaite/public members

* Remove broadcasting step from a contract transaction

* Cleanup uneeded types

* Refactor ens-base to typescript and add typings for ENS smart contracts

* Migrate ens-name-search to TS

* Add IResolveDomainRequest

* Fix rest of TSC errors

* Add definition file for bn.js

* Remove types-bn

* Fix some typings

* make isBN a static property

* progress commit -- swap out bignumber.js for bn.js

* Swap out bignumber for bn in vendor

* Change modn to number return

* Start to strip out units lib for a string manipulation based lib

* Convert codebase to only base units

* Get rid of useless component

* Handle only wei in values

* Use unit conversion in sidebar

* Automatically strip hex prefix, and  handle decimal edge case

* Handle base 16 wei in transactions

* Make a render callback component for dealing with unit conversion

* Switch contracts to use bn.js, and get transaction values from signedTx instead of state

* Get send transaction  working with bn.js

* Remove redundant hex stripping,  return base value of tokens

* Cleanup unit file

* Re-implement toFixed for strings

* Use formatNumber in codebase

* Cleanup code

* Undo package test changes

* Update snapshot and remove console logs

* Use TokenValue / Wei more consistently where applicable

* Add typing to deterministicWallets, fix confirmation modal, make UnitDisplay more flexible

* Split different ENS modes into their own components

* Fix Abi typedef

* Remove redundant moment type package

* Add Aux helper component

* Split out resolve components

* Make 'to' parameter optional

* Change import type

* Change typing to be base domain request

* Split handling of resolving into object handler

* Fix countdown component

* Adjust element spacing

* Implement reveal search functionality

* Add unit display for highest bidder

* Fill out forbidden/NYA modes

* ENS wallet component skeleton

* Clean up prop handling in UnitDisplay

* Change instanceof to typeof check, change boolean of displayBalance

* Add ENS wallet component

* Cleanup spacing

* Convert ConfModal for bidding in ENS

* Make ui component for placing bids

* Fix destructure in placeBid

* Pass through entire wallet

* Remove text center

* Display inline notification ENS isValid & add some ENS tests

* Add export of Aux

* Reformat with prettier

* progress...

* Add ENSUnlockLayout

* Add RevealBid component

* organize NameResolve components

* Merge ENS with transaction-refactor changes

* Fix address resolution

* Update styles

* convert ens name to lowercase before checking

* Add overflow-y:scroll to table

* update ens snapshots & tests

* cast 'undefined' state argument as any for testing

* clean up components

* Connect unitconverter to redux state

* remove unnecessary type assertion

* fix spinner size

* remove old bidmodal

* validate bidmask before opening modal

* progress...

* Update styles

* Add saga / actions for placing a bid

* Update types & clean up dead code

* Delete old test

* Dispatch PlaceBidRequested acitons

* Progress commit -- get ENS bidding ready for tx generation via sagas

* Seperate ENS action creators and types

* Add reducer & actions for ENS fields

* Add preliminary sagas for bid mask and bid value

* Initial commit

* Add loading indicator

* Remove some bidding components

* Revert bidding files

* Remove more bidding code

* Remove rest of bidding code

* Fix ENS error message

* Revert value saga changes

* Remove error param from setting 'To' field

* Fix existing ENS test

* Cleanup address resolution, remove dead code

* Remove error messages from unimplemented ENS

* Fix last character being not set bug

* Remove error state from Meta

* Rename isGenesisAddress to isCreationAddress
This commit is contained in:
HenryNguyen5 2018-01-15 04:57:09 -05:00 committed by Daniel Ternyak
parent eb4fd1cce8
commit 67b2e6491c
64 changed files with 1771 additions and 261 deletions

View File

@ -1,22 +0,0 @@
import * as interfaces from './actionTypes';
import * as constants from './constants';
export function resolveEnsName(name: string): interfaces.ResolveEnsNameAction {
return {
type: constants.ENS_RESOLVE,
payload: name
};
}
export function cacheEnsAddress(
ensName: string,
address: string
): interfaces.CacheEnsAddressAction {
return {
type: constants.ENS_CACHE,
payload: {
ensName,
address
}
};
}

View File

@ -0,0 +1 @@
export * from './resolveDomain';

View File

@ -0,0 +1,35 @@
import * as ActionTypes from '../actionTypes';
import { TypeKeys } from '../constants';
import { DomainRequest } from 'libs/ens';
import { ResolveDomainCached } from 'actions/ens';
export type TResolveDomainRequested = typeof resolveDomainRequested;
export const resolveDomainRequested = (domain: string): ActionTypes.ResolveDomainRequested => ({
type: TypeKeys.ENS_RESOLVE_DOMAIN_REQUESTED,
payload: { domain }
});
export const resolveDomainCached = (
payload: ResolveDomainCached['payload']
): ResolveDomainCached => ({
type: TypeKeys.ENS_RESOLVE_DOMAIN_CACHED,
payload
});
export type TResolveDomainSucceeded = typeof resolveDomainSucceeded;
export const resolveDomainSucceeded = (
domain: string,
domainData: DomainRequest
): ActionTypes.ResolveDomainSucceeded => ({
type: TypeKeys.ENS_RESOLVE_DOMAIN_SUCCEEDED,
payload: { domain, domainData }
});
export type TResolveDomainFailed = typeof resolveDomainFailed;
export const resolveDomainFailed = (
domain: string,
error: Error
): ActionTypes.ResolveDomainFailed => ({
type: TypeKeys.ENS_RESOLVE_DOMAIN_FAILED,
payload: { domain, error }
});

View File

@ -1,17 +0,0 @@
/*** Resolve ENS name ***/
export interface ResolveEnsNameAction {
type: 'ENS_RESOLVE';
payload: string;
}
/*** Cache ENS address ***/
export interface CacheEnsAddressAction {
type: 'ENS_CACHE';
payload: {
ensName: string;
address: string;
};
}
/*** Union Type ***/
export type EnsAction = ResolveEnsNameAction | CacheEnsAddressAction;

View File

@ -0,0 +1,5 @@
import { ResolveDomainAction } from './resolveDomain';
export * from './resolveDomain';
export type EnsAction = ResolveDomainAction;

View File

@ -0,0 +1 @@
export * from './actionTypes';

View File

@ -0,0 +1,28 @@
import { TypeKeys } from '../constants';
import { DomainRequest } from 'libs/ens';
export interface ResolveDomainRequested {
type: TypeKeys.ENS_RESOLVE_DOMAIN_REQUESTED;
payload: { domain: string };
}
export interface ResolveDomainSucceeded {
type: TypeKeys.ENS_RESOLVE_DOMAIN_SUCCEEDED;
payload: { domain: string; domainData: DomainRequest };
}
export interface ResolveDomainCached {
type: TypeKeys.ENS_RESOLVE_DOMAIN_CACHED;
payload: { domain: string };
}
export interface ResolveDomainFailed {
type: TypeKeys.ENS_RESOLVE_DOMAIN_FAILED;
payload: { domain: string; error: Error };
}
export type ResolveDomainAction =
| ResolveDomainRequested
| ResolveDomainSucceeded
| ResolveDomainFailed
| ResolveDomainCached;

View File

@ -1,2 +1,6 @@
export const ENS_RESOLVE = 'ENS_RESOLVE';
export const ENS_CACHE = 'ENS_CACHE';
export enum TypeKeys {
ENS_RESOLVE_DOMAIN_REQUESTED = 'ENS_RESOLVE_DOMAIN_REQUESTED',
ENS_RESOLVE_DOMAIN_SUCCEEDED = 'ENS_RESOLVE_DOMAIN_SUCCEEDED',
ENS_RESOLVE_DOMAIN_CACHED = 'ENS_RESOLVE_DOMAIN_CACHED',
ENS_RESOLVE_DOMAIN_FAILED = 'ENS_RESOLVE_DOMAIN_FAILED'
}

View File

@ -52,7 +52,6 @@ interface SetToFieldAction {
payload: {
raw: string;
value: Address | null;
error?: string | null;
};
}

View File

@ -7,7 +7,6 @@ interface SetTokenToMetaAction {
payload: {
raw: string;
value: Address | null;
error?: string | null;
};
}

View File

@ -8,7 +8,7 @@ interface Props {
export const AddressField: React.SFC<Props> = ({ isReadOnly }) => (
<AddressFieldFactory
withProps={({ currentTo, isValid, onChange, readOnly, errorMsg }) => (
withProps={({ currentTo, isValid, onChange, readOnly }) => (
<React.Fragment>
<input
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
@ -18,11 +18,6 @@ export const AddressField: React.SFC<Props> = ({ isReadOnly }) => (
readOnly={!!(isReadOnly || readOnly)}
onChange={onChange}
/>
{errorMsg && (
<div className="has-error">
<span className="help-block">{errorMsg}</span>
</div>
)}
</React.Fragment>
)}
/>

View File

@ -18,14 +18,12 @@ export interface CallbackProps {
isValid: boolean;
readOnly: boolean;
currentTo: ICurrentTo;
errorMsg?: string | null;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
type Props = DispatchProps & DispatchProps & OwnProps;
type Props = DispatchProps & OwnProps;
//TODO: add ens resolving
class AddressFieldFactoryClass extends React.Component<Props, {}> {
class AddressFieldFactoryClass extends React.Component<Props> {
public componentDidMount() {
// this 'to' parameter can be either token or actual field related
const { to } = this.props;
@ -44,14 +42,17 @@ class AddressFieldFactoryClass extends React.Component<Props, {}> {
};
}
const AddressField = connect(null, { setCurrentTo })(AddressFieldFactoryClass);
const AddressFieldFactory = connect(null, { setCurrentTo })(AddressFieldFactoryClass);
interface DefaultAddressFieldProps {
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({ withProps }) => (
<Query params={['to']} withQuery={({ to }) => <AddressField to={to} withProps={withProps} />} />
<Query
params={['to']}
withQuery={({ to }) => <AddressFieldFactory to={to} withProps={withProps} />}
/>
);
export { DefaultAddressField as AddressFieldFactory };

View File

@ -1,29 +1,51 @@
import React, { Component } from 'react';
import { Identicon } from 'components/ui';
import { Identicon, Spinner } from 'components/ui';
import translate from 'translations';
//import { EnsAddress } from './components';
import { Query } from 'components/renderCbs';
import { ICurrentTo, getCurrentTo, isValidCurrentTo } from 'selectors/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { CallbackProps } from 'components/AddressFieldFactory';
import { addHexPrefix } from 'ethereumjs-util';
import { getResolvingDomain } from 'selectors/ens';
import { isValidENSAddress } from 'libs/validators';
interface StateProps {
currentTo: ICurrentTo;
isValid: boolean;
isResolving: boolean;
}
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>): void;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const ENSStatus: React.SFC<{ isLoading: boolean; ensAddress: string; rawAddress: string }> = ({
isLoading,
ensAddress,
rawAddress
}) => {
const isENS = isValidENSAddress(ensAddress);
const text = 'Loading ENS address...';
if (isLoading) {
return (
<>
<Spinner /> {text}
</>
);
} else {
return isENS ? <>{`Resolved Address: ${rawAddress}`}</> : null;
}
};
type Props = OwnProps & StateProps;
//TODO: ENS handling
class AddressInputFactoryClass extends Component<Props> {
public render() {
const { currentTo, onChange, isValid, withProps } = this.props;
const { raw } = currentTo;
const { currentTo, onChange, isValid, withProps, isResolving } = this.props;
const { value } = currentTo;
const addr = addHexPrefix(value ? value.toString('hex') : '0');
return (
<div className="row form-group">
<div className="col-xs-11">
@ -35,15 +57,14 @@ class AddressInputFactoryClass extends Component<Props> {
currentTo,
isValid,
onChange,
readOnly: !!readOnly,
errorMsg: currentTo.error
readOnly: !!readOnly || this.props.isResolving
})
}
/>
{/*<EnsAddress ensAddress={ensAddress} />*/}
<ENSStatus ensAddress={currentTo.raw} isLoading={isResolving} rawAddress={addr} />
</div>
<div className="col-xs-1" style={{ padding: 0 }}>
<Identicon address={/*ensAddress ||*/ raw} />
<Identicon address={addr} />
</div>
</div>
);
@ -52,5 +73,6 @@ class AddressInputFactoryClass extends Component<Props> {
export const AddressInputFactory = connect((state: AppState) => ({
currentTo: getCurrentTo(state),
isResolving: getResolvingDomain(state),
isValid: isValidCurrentTo(state)
}))(AddressInputFactoryClass);

View File

@ -1,36 +0,0 @@
import React from 'react';
/*
public onChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
const newValue = (e.target as HTMLInputElement).value;
const { onChange } = this.props;
if (!onChange) {
return;
}
// FIXME debounce?
if (isValidENSAddress(newValue)) {
this.props.resolveEnsName(newValue);
}
onChange(newValue);
};
}
function mapStateToProps(state: AppState, props: PublicProps) {
return {
ensAddress: getEnsAddress(state, props.value)
};
}
export default connect(mapStateToProps, { resolveEnsName })(AddressField);
*/
interface EnsAddressProps {
ensAddress: string | null;
}
export const EnsAddress: React.SFC<EnsAddressProps> = ({ ensAddress }) =>
(!!ensAddress && (
<p className="ens-response">
<span className="mono">{ensAddress}</span>
</p>
)) ||
null;

View File

@ -1 +0,0 @@
export * from './EnsAddress';

View File

@ -142,13 +142,6 @@
font-size: $font-size-small;
margin: $space-sm 0;
}
@media screen and (max-width: $grid-float-breakpoint) {
.row {
margin-left: -0.5rem;
margin-right: -0.5rem;
}
}
}
.Modal {

View File

@ -1,9 +1,11 @@
import { toTokenBase } from 'libs/units';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getDecimal } from 'selectors/transaction';
interface IChildren {
onUserInput: UnitConverter['onUserInput'];
onUserInput: UnitConverterClass['onUserInput'];
convertedUnit: string;
}
interface IFakeEvent {
@ -24,7 +26,7 @@ interface State {
const initialState = { userInput: '' };
export class UnitConverter extends Component<Props, State> {
class UnitConverterClass extends Component<Props, State> {
public state: State = initialState;
public componentWillReceiveProps(nextProps: Props) {
@ -58,3 +60,11 @@ export class UnitConverter extends Component<Props, State> {
this.props.onChange(fakeEvent);
};
}
const mapStateToProps = (state: AppState) => {
return {
decimal: getDecimal(state)
};
};
export const UnitConverter = connect(mapStateToProps)(UnitConverterClass);

View File

@ -10,9 +10,7 @@ interface Props {
export default function Identicon(props: Props) {
const size = props.size || '4rem';
// FIXME breaks on failed checksums
const identiconDataUrl = isValidETHAddress(props.address.toLowerCase())
? toDataUrl(props.address.toLowerCase())
: '';
const identiconDataUrl = isValidETHAddress(props.address) ? toDataUrl(props.address) : '';
return (
<div style={{ position: 'relative', width: size, height: size }} title="Address Identicon">
<div

View File

@ -6,7 +6,6 @@ const ABIFUNC_METHOD_NAMES = ['encodeInput', 'decodeInput', 'decodeOutput'];
enum ABIMethodTypes {
FUNC = 'function'
}
export type TContract = typeof Contract;
export default class Contract {

View File

@ -1,9 +0,0 @@
import uts46 from 'idna-uts46';
export function normalise(name: string): string {
try {
return uts46.toUnicode(name, { useStd3ASCII: true, transitional: false });
} catch (e) {
throw e;
}
}

12
common/libs/ens/contracts/AbiFunc.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
export interface ABIFunc<T, K = void> {
outputType: K;
encodeInput(x: T): string;
decodeOutput(argStr: string): K;
}
export interface ABIFuncParamless<T = void> {
outputType: T;
encodeInput(): string;
decodeOutput(argStr: string): T;
}

View File

@ -0,0 +1,49 @@
import { ABIFunc, ABIFuncParamless } from '../AbiFunc';
export interface IAuction {
releaseDeed: ABIFunc<{ _hash: bytes32 }>;
getAllowedTime: ABIFunc<{ _hash: bytes32 }, { timestamp: uint256 }>;
invalidateName: ABIFunc<{ unhashedName: string }>;
shaBid: ABIFunc<
{ hash: bytes32; owner: address; value: uint256; salt: bytes32 },
{ sealedBid: bytes32 }
>;
cancelBid: ABIFunc<{ bidder: address; seal: bytes32 }>;
entries: ABIFunc<
{ _hash: bytes32 },
{
mode: uint8;
deedAddress: address;
registrationDate: uint256;
value: uint256;
highestBid: uint256;
}
>;
ens: ABIFuncParamless<{ ensAddress: address }>;
unsealBid: ABIFunc<{ _hash: bytes32; _value: uint256; _salt: bytes32 }>;
transferRegistrars: ABIFunc<{ _hash: bytes32 }>;
sealedBids: ABIFunc<{ address_0: address; bytes32_1: bytes32 }, { deedAddress: address }>;
state: ABIFunc<{ _hash: bytes32 }, { state: uint8 }>;
transfer: ABIFunc<{ _hash: bytes32; newOwner: address }>;
isAllowed: ABIFunc<{ _hash: bytes32; _timestamp: uint256 }, { allowed: bool }>;
finalizeAuction: ABIFunc<{ _hash: bytes32 }>;
registryStarted: ABIFuncParamless<{ registryStartDate: uint256 }>;
launchLength: ABIFuncParamless<{ launchLength: uint32 }>;
newBid: ABIFunc<{ sealedBid: bytes32 }>;
eraseNode: ABIFunc<{ labels: bytes32[] }>;
startAuctions: ABIFunc<{ _hashes: bytes32[] }>;
acceptRegistrarTransfer: ABIFunc<{
hash: bytes32;
deed: address;
registrationDate: uint256;
}>;
startAuction: ABIFunc<{ _hash: bytes32 }>;
rootNode: ABIFuncParamless<{ rootNode: bytes32 }>;
startAuctionsAndBid: ABIFunc<{ hashes: bytes32[]; sealedBid: bytes32 }>;
}
type bytes32 = any;
type uint256 = any;
type address = any;
type uint8 = any;
type bool = boolean;
type uint32 = any;

View File

@ -0,0 +1,550 @@
[
{
"constant": false,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
}
],
"name": "releaseDeed",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
}
],
"name": "getAllowedTime",
"outputs": [
{
"name": "timestamp",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "unhashedName",
"type": "string"
}
],
"name": "invalidateName",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "hash",
"type": "bytes32"
},
{
"name": "owner",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "salt",
"type": "bytes32"
}
],
"name": "shaBid",
"outputs": [
{
"name": "sealedBid",
"type": "bytes32"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "bidder",
"type": "address"
},
{
"name": "seal",
"type": "bytes32"
}
],
"name": "cancelBid",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
}
],
"name": "entries",
"outputs": [
{
"name": "",
"type": "uint8"
},
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "ens",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
},
{
"name": "_value",
"type": "uint256"
},
{
"name": "_salt",
"type": "bytes32"
}
],
"name": "unsealBid",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
}
],
"name": "transferRegistrars",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "bytes32"
}
],
"name": "sealedBids",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
}
],
"name": "state",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
},
{
"name": "newOwner",
"type": "address"
}
],
"name": "transfer",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
},
{
"name": "_timestamp",
"type": "uint256"
}
],
"name": "isAllowed",
"outputs": [
{
"name": "allowed",
"type": "bool"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
}
],
"name": "finalizeAuction",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "registryStarted",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "launchLength",
"outputs": [
{
"name": "",
"type": "uint32"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "sealedBid",
"type": "bytes32"
}
],
"name": "newBid",
"outputs": [],
"payable": true,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "labels",
"type": "bytes32[]"
}
],
"name": "eraseNode",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_hashes",
"type": "bytes32[]"
}
],
"name": "startAuctions",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "hash",
"type": "bytes32"
},
{
"name": "deed",
"type": "address"
},
{
"name": "registrationDate",
"type": "uint256"
}
],
"name": "acceptRegistrarTransfer",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_hash",
"type": "bytes32"
}
],
"name": "startAuction",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "rootNode",
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "hashes",
"type": "bytes32[]"
},
{
"name": "sealedBid",
"type": "bytes32"
}
],
"name": "startAuctionsAndBid",
"outputs": [],
"payable": true,
"type": "function"
},
{
"inputs": [
{
"name": "_ens",
"type": "address"
},
{
"name": "_rootNode",
"type": "bytes32"
},
{
"name": "_startDate",
"type": "uint256"
}
],
"payable": false,
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "hash",
"type": "bytes32"
},
{
"indexed": false,
"name": "registrationDate",
"type": "uint256"
}
],
"name": "AuctionStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "hash",
"type": "bytes32"
},
{
"indexed": true,
"name": "bidder",
"type": "address"
},
{
"indexed": false,
"name": "deposit",
"type": "uint256"
}
],
"name": "NewBid",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "hash",
"type": "bytes32"
},
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "status",
"type": "uint8"
}
],
"name": "BidRevealed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "hash",
"type": "bytes32"
},
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "registrationDate",
"type": "uint256"
}
],
"name": "HashRegistered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "hash",
"type": "bytes32"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "HashReleased",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "hash",
"type": "bytes32"
},
{
"indexed": true,
"name": "name",
"type": "string"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "registrationDate",
"type": "uint256"
}
],
"name": "HashInvalidated",
"type": "event"
}
]

View File

@ -0,0 +1,9 @@
export default {
ens: ['ensAddress'],
entries: ['mode', 'deedAddress', 'registrationDate', 'value', 'highestBid'],
sealedBids: ['deedAddress'],
state: ['state'],
registryStarted: ['registryStartDate'],
launchLength: ['launchLength'],
rootNode: ['rootNode']
};

View File

@ -0,0 +1,14 @@
import { ABIFunc, ABIFuncParamless } from '../AbiFunc';
export interface IDeed {
creationDate: ABIFuncParamless<{ creationDate: uint256 }>;
destroyDeed: ABIFuncParamless;
setOwner: ABIFunc<{ newOwner: address }>;
registrar: ABIFuncParamless<{ registrarAddress: address }>;
owner: ABIFuncParamless<{ ownerAddress: address }>;
closeDeed: ABIFunc<{ refundRatio: uint256 }>;
setRegistrar: ABIFunc<{ newRegistrar: address }>;
setBalance: ABIFunc<{ newValue: uint256 }>;
}
type uint256 = any;
type address = any;

View File

@ -0,0 +1,98 @@
[{
"constant": true,
"inputs": [],
"name": "creationDate",
"outputs": [{
"name": "",
"type": "uint256"
}],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [],
"name": "destroyDeed",
"outputs": [],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "newOwner",
"type": "address"
}],
"name": "setOwner",
"outputs": [],
"payable": false,
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "registrar",
"outputs": [{
"name": "",
"type": "address"
}],
"payable": false,
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [{
"name": "",
"type": "address"
}],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "refundRatio",
"type": "uint256"
}],
"name": "closeDeed",
"outputs": [],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "newRegistrar",
"type": "address"
}],
"name": "setRegistrar",
"outputs": [],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "newValue",
"type": "uint256"
}],
"name": "setBalance",
"outputs": [],
"payable": true,
"type": "function"
}, {
"inputs": [],
"type": "constructor"
}, {
"payable": true,
"type": "fallback"
}, {
"anonymous": false,
"inputs": [{
"indexed": false,
"name": "newOwner",
"type": "address"
}],
"name": "OwnerChanged",
"type": "event"
}, {
"anonymous": false,
"inputs": [],
"name": "DeedClosed",
"type": "event"
}]

View File

@ -0,0 +1,5 @@
export default {
creationDate: ['creationDate'],
registrar: ['registrarAddress'],
owner: ['ownerAddress']
};

View File

@ -0,0 +1,26 @@
import Contract from 'libs/contracts';
const auctionABI = require('./auction/auction.json');
import auctionOutputMappings from './auction/outputMappings';
import { IAuction } from './auction/auction';
const deedABI = require('./deed/deed.json');
import deedOutputMappings from './deed/outputMappings';
import { IDeed } from './deed/deed';
const registryABI = require('./registry/registry.json');
import registryOutputMappings from './registry/outputMappings';
import { IRegistry } from './registry/registry';
const resolverABI = require('./resolver/resolver.json');
import resolverOutputMappings from './resolver/outputMappings';
import { IResolver } from './resolver/resolver';
const auction: IAuction & Contract = new Contract(auctionABI, auctionOutputMappings) as any;
const deed: IDeed & Contract = new Contract(deedABI, deedOutputMappings) as any;
const registry: IRegistry & Contract = new Contract(registryABI, registryOutputMappings) as any;
const resolver: IResolver & Contract = new Contract(resolverABI, resolverOutputMappings) as any;
export default { auction, deed, registry, resolver };

View File

@ -0,0 +1,5 @@
export default {
resolver: ['resolverAddress'],
owner: ['ownerAddress'],
ttl: ['timeToLive']
};

View File

@ -0,0 +1,15 @@
import { ABIFunc, ABIFuncParamless } from '../AbiFunc';
export interface IRegistry {
resolver: ABIFunc<{ node: bytes32 }, { resolverAddress: address }>;
owner: ABIFunc<{ node: bytes32 }, { ownerAddress: address }>;
setSubnodeOwner: ABIFunc<{ node: bytes32; label: bytes32; owner: address }>;
setTTL: ABIFunc<{ node: bytes32; ttl: uint64 }>;
ttl: ABIFunc<{ node: bytes32 }, { timeToLive: uint64 }>;
setResolver: ABIFunc<{ node: bytes32; resolver: address }>;
setOwner: ABIFunc<{ node: bytes32; owner: address }>;
}
type bytes32 = any;
type address = any;
type uint64 = any;

View File

@ -0,0 +1,151 @@
[{
"constant": true,
"inputs": [{
"name": "node",
"type": "bytes32"
}],
"name": "resolver",
"outputs": [{
"name": "",
"type": "address"
}],
"payable": false,
"type": "function"
}, {
"constant": true,
"inputs": [{
"name": "node",
"type": "bytes32"
}],
"name": "owner",
"outputs": [{
"name": "",
"type": "address"
}],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "node",
"type": "bytes32"
}, {
"name": "label",
"type": "bytes32"
}, {
"name": "owner",
"type": "address"
}],
"name": "setSubnodeOwner",
"outputs": [],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "node",
"type": "bytes32"
}, {
"name": "ttl",
"type": "uint64"
}],
"name": "setTTL",
"outputs": [],
"payable": false,
"type": "function"
}, {
"constant": true,
"inputs": [{
"name": "node",
"type": "bytes32"
}],
"name": "ttl",
"outputs": [{
"name": "",
"type": "uint64"
}],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "node",
"type": "bytes32"
}, {
"name": "resolver",
"type": "address"
}],
"name": "setResolver",
"outputs": [],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "node",
"type": "bytes32"
}, {
"name": "owner",
"type": "address"
}],
"name": "setOwner",
"outputs": [],
"payable": false,
"type": "function"
}, {
"anonymous": false,
"inputs": [{
"indexed": true,
"name": "node",
"type": "bytes32"
}, {
"indexed": false,
"name": "owner",
"type": "address"
}],
"name": "Transfer",
"type": "event"
}, {
"anonymous": false,
"inputs": [{
"indexed": true,
"name": "node",
"type": "bytes32"
}, {
"indexed": true,
"name": "label",
"type": "bytes32"
}, {
"indexed": false,
"name": "owner",
"type": "address"
}],
"name": "NewOwner",
"type": "event"
}, {
"anonymous": false,
"inputs": [{
"indexed": true,
"name": "node",
"type": "bytes32"
}, {
"indexed": false,
"name": "resolver",
"type": "address"
}],
"name": "NewResolver",
"type": "event"
}, {
"anonymous": false,
"inputs": [{
"indexed": true,
"name": "node",
"type": "bytes32"
}, {
"indexed": false,
"name": "ttl",
"type": "uint64"
}],
"name": "NewTTL",
"type": "event"
}]

View File

@ -0,0 +1,4 @@
export default {
supportsInterface: ['doesSupportInterface'],
has: ['has']
};

View File

@ -0,0 +1,15 @@
import { ABIFunc, ABIFuncParamless } from '../AbiFunc';
export interface IResolver {
supportsInterface: ABIFunc<{ interfaceID: bytes4 }, { doesSupportInterface: bool }>;
addr: ABIFunc<{ node: bytes32 }, { ret: address }>;
has: ABIFunc<{ node: bytes32; kind: bytes32 }, { has: bool }>;
setAddr: ABIFunc<{ node: bytes32; addr: address }>;
content: ABIFunc<{ node: bytes32 }, { ret: bytes32 }>;
setContent: ABIFunc<{ node: bytes32; hash: bytes32 }>;
}
type bytes4 = any;
type bool = boolean;
type bytes32 = any;
type address = any;

View File

@ -0,0 +1,91 @@
[{
"constant": true,
"inputs": [{
"name": "interfaceID",
"type": "bytes4"
}],
"name": "supportsInterface",
"outputs": [{
"name": "",
"type": "bool"
}],
"payable": false,
"type": "function"
}, {
"constant": true,
"inputs": [{
"name": "node",
"type": "bytes32"
}],
"name": "addr",
"outputs": [{
"name": "ret",
"type": "address"
}],
"payable": false,
"type": "function"
}, {
"constant": true,
"inputs": [{
"name": "node",
"type": "bytes32"
}, {
"name": "kind",
"type": "bytes32"
}],
"name": "has",
"outputs": [{
"name": "",
"type": "bool"
}],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "node",
"type": "bytes32"
}, {
"name": "addr",
"type": "address"
}],
"name": "setAddr",
"outputs": [],
"payable": false,
"type": "function"
}, {
"constant": true,
"inputs": [{
"name": "node",
"type": "bytes32"
}],
"name": "content",
"outputs": [{
"name": "ret",
"type": "bytes32"
}],
"payable": false,
"type": "function"
}, {
"constant": false,
"inputs": [{
"name": "node",
"type": "bytes32"
}, {
"name": "hash",
"type": "bytes32"
}],
"name": "setContent",
"outputs": [],
"payable": false,
"type": "function"
}, {
"inputs": [{
"name": "ensAddr",
"type": "address"
}],
"type": "constructor"
}, {
"payable": false,
"type": "fallback"
}]

86
common/libs/ens/index.ts Normal file
View File

@ -0,0 +1,86 @@
import uts46 from 'idna-uts46';
import ethUtil from 'ethereumjs-util';
export function normalise(name: string) {
try {
return uts46.toUnicode(name, { useStd3ASCII: true, transitional: false });
} catch (e) {
throw e;
}
}
export const getNameHash = (name: string = ''): string => {
if (name === '') {
throw new Error('Empty string provided');
}
const normalizedName = normalise(name);
const sha3 = ethUtil.sha3;
const labels = normalizedName.split('.');
const emptyNode = Buffer.alloc(32);
const rawNode = labels.reduceRight((node, currentLabel) => {
return sha3(Buffer.concat([node, sha3(currentLabel)]));
}, emptyNode);
return `0x${rawNode.toString('hex')}`;
};
export interface IBaseDomainRequest {
name: string;
labelHash: string;
mode: NameState;
highestBid: string;
value: string;
deedAddress: string;
registrationDate: string;
nameHash: string;
mappedMode: string;
}
export interface IOwnedDomainRequest extends IBaseDomainRequest {
ownerAddress: string;
resolvedAddress: string;
}
export interface IRevealDomainRequest extends IBaseDomainRequest {
ownerAddress: string;
}
export type DomainRequest = IOwnedDomainRequest | IRevealDomainRequest | IBaseDomainRequest;
export interface IDomainData<Mode> {
mode: Mode;
deedAddress: string;
registrationDate: string;
value: string;
highestBid: string;
}
export enum NameState {
Open = '0',
Auction = '1',
Owned = '2',
Forbidden = '3',
Reveal = '4',
NotYetAvailable = '5'
}
export const modeStrMap = name => [
`${name} is available and the auction hasnt started`,
`${name} is available and the auction has been started`,
`${name} is taken and currently owned by someone`,
`${name} is forbidden`,
`${name} is currently in the reveal stage of the auction`,
`${name} is not yet available due to the soft launch of names.`
];
export interface IModeMap {
[x: string]: (
domainData: IDomainData<NameState>,
nameHash?: string,
hash?: Buffer
) =>
| {}
| { ownerAddress: string; resolvedAddress: string }
| { auctionCloseTime: string; revealBidTime: string };
}

View File

@ -0,0 +1,15 @@
const main: IEnsAddresses = require('./main.json');
const rinkeby: IEnsAddresses = require('./rinkeby.json');
const ropsten: IEnsAddresses = require('./ropsten.json');
interface IEnsAddresses {
public: {
resolver: string;
reverse: string;
ethAuction: string;
};
registry: string;
}
export default { main, rinkeby, ropsten };

View File

@ -0,0 +1,8 @@
{
"public": {
"resolver": "0x5FfC014343cd971B7eb70732021E26C35B744cc4",
"reverse": "0x9062c0a6dbd6108336bcbe4593a3d1ce05512069",
"ethAuction": "0x6090a6e47849629b7245dfa1ca21d94cd15878ef"
},
"registry": "0x314159265dD8dbb310642f98f50C066173C1259b"
}

View File

@ -0,0 +1,8 @@
{
"public": {
"resolver": "0xb14fdee4391732ea9d2267054ead2084684c0ad8",
"reverse": "0x0000000000000000000000000000000000000000",
"ethAuction": "0x0000000000000000000000000000000000000000"
},
"registry": "0xe7410170f87102df0055eb195163a03b7f2bff4a"
}

View File

@ -0,0 +1,8 @@
{
"public": {
"resolver": "0x4c641fb9bad9b60ef180c31f56051ce826d21a9a",
"reverse": "0xdb6cead81ce14a63c284728eed17738a81327ff0",
"ethAuction": "0xc19fd9004b5c9789391679de6d766b981db94610"
},
"registry": "0x112234455c3a32fd11230c42e7bccd4a84e02010"
}

View File

@ -22,6 +22,9 @@ export function isValidETHAddress(address: string): boolean {
}
}
export const isCreationAddress = (address: string): boolean =>
address === '0x0' || address === '0x0000000000000000000000000000000000000000';
export function isValidBTCAddress(address: string): boolean {
return WalletAddressValidator.validate(address, 'BTC');
}

View File

@ -1,21 +0,0 @@
import { CacheEnsAddressAction, EnsAction } from 'actions/ens';
export interface State {
[key: string]: string;
}
export const INITIAL_STATE: State = {};
function cacheEnsAddress(state: State, action: CacheEnsAddressAction): State {
const { ensName, address } = action.payload;
return { ...state, [ensName]: address };
}
export function ens(state: State = INITIAL_STATE, action: EnsAction): State {
switch (action.type) {
case 'ENS_CACHE':
return cacheEnsAddress(state, action);
default:
return state;
}
}

View File

@ -0,0 +1,82 @@
import {
EnsAction,
ResolveDomainRequested,
ResolveDomainFailed,
ResolveDomainSucceeded,
ResolveDomainCached
} from 'actions/ens';
import { DomainRequest } from 'libs/ens';
import { TypeKeys } from 'actions/ens/constants';
export interface State {
[key: string]: {
state: REQUEST_STATES;
data?: DomainRequest;
error?: boolean;
errorMsg?: string;
};
}
const INITIAL_STATE: State = {};
export enum REQUEST_STATES {
pending = 'PENDING',
success = 'SUCCESS',
failed = 'FAILED'
}
const resolveDomainRequested = (state: State, action: ResolveDomainRequested): State => {
const { domain } = action.payload;
const nextDomain = {
...state[domain],
state: REQUEST_STATES.pending
};
return { ...state, [domain]: nextDomain };
};
const resolveDomainSuccess = (state: State, action: ResolveDomainSucceeded): State => {
const { domain, domainData } = action.payload;
const nextDomain = {
data: domainData,
state: REQUEST_STATES.success
};
return { ...state, [domain]: nextDomain };
};
const resolveDomainCached = (state: State, action: ResolveDomainCached): State => {
const { domain } = action.payload;
const nextDomain = {
...state[domain],
state: REQUEST_STATES.success
};
return { ...state, [domain]: nextDomain };
};
const resolveDomainFailed = (state: State, action: ResolveDomainFailed): State => {
const { domain, error } = action.payload;
const nextDomain = {
error: true,
errorMsg: error.message,
state: REQUEST_STATES.failed
};
return { ...state, [domain]: nextDomain };
};
export default (state: State = INITIAL_STATE, action: EnsAction): State => {
switch (action.type) {
case TypeKeys.ENS_RESOLVE_DOMAIN_REQUESTED:
return resolveDomainRequested(state, action);
case TypeKeys.ENS_RESOLVE_DOMAIN_SUCCEEDED:
return resolveDomainSuccess(state, action);
case TypeKeys.ENS_RESOLVE_DOMAIN_FAILED:
return resolveDomainFailed(state, action);
case TypeKeys.ENS_RESOLVE_DOMAIN_CACHED:
return resolveDomainCached(state, action);
default:
return state;
}
};

View File

@ -0,0 +1,40 @@
import {
EnsAction,
ResolveDomainSucceeded,
ResolveDomainCached,
ResolveDomainRequested
} from 'actions/ens';
import { TypeKeys } from 'actions/ens/constants';
export interface State {
currentDomain: null | string;
}
const INITIAL_STATE: State = {
currentDomain: null
};
const setCurrentDomainName = (
state: State,
action: ResolveDomainSucceeded | ResolveDomainCached | ResolveDomainRequested
): State => {
const { domain: domainName } = action.payload;
return { ...state, currentDomain: domainName };
};
const clearCurrentDomainName = (): State => {
return { currentDomain: null };
};
export default (state: State = INITIAL_STATE, action: EnsAction): State => {
switch (action.type) {
case TypeKeys.ENS_RESOLVE_DOMAIN_CACHED:
case TypeKeys.ENS_RESOLVE_DOMAIN_REQUESTED:
case TypeKeys.ENS_RESOLVE_DOMAIN_SUCCEEDED:
return setCurrentDomainName(state, action);
case TypeKeys.ENS_RESOLVE_DOMAIN_FAILED:
return clearCurrentDomainName();
default:
return state;
}
};

View File

@ -0,0 +1,11 @@
import domainSelector, { State as DSState } from './domainSelector';
import domainRequests, { State as DRState } from './domainRequests';
import { combineReducers } from 'redux';
export interface State {
domainSelector: DSState;
domainRequests: DRState;
}
export const ens = combineReducers({ domainSelector, domainRequests });

View File

@ -13,7 +13,7 @@ import { State } from './typings';
import { gasPricetoBase } from 'libs/units';
const INITIAL_STATE: State = {
to: { raw: '', value: null, error: null },
to: { raw: '', value: null },
data: { raw: '', value: null },
nonce: { raw: '', value: null },
value: { raw: '', value: null },

View File

@ -18,7 +18,7 @@ const INITIAL_STATE: State = {
previousUnit: 'ether',
decimal: getDecimalFromEtherUnit('ether'),
tokenValue: { raw: '', value: null },
tokenTo: { raw: '', value: null, error: null },
tokenTo: { raw: '', value: null },
from: null
};

68
common/sagas/ens/ens.ts Normal file
View File

@ -0,0 +1,68 @@
import {
resolveDomainFailed,
resolveDomainSucceeded,
ResolveDomainRequested,
resolveDomainCached
} from 'actions/ens';
import { TypeKeys } from 'actions/ens/constants';
import { SagaIterator, delay, buffers } from 'redux-saga';
import { INode } from 'libs/nodes/INode';
import { getNodeLib } from 'selectors/config';
import { call, put, select, all, actionChannel, take, fork, race } from 'redux-saga/effects';
import { showNotification } from 'actions/notifications';
import { resolveDomainRequest } from './modeMap';
import { getCurrentDomainName, getCurrentDomainData } from 'selectors/ens';
function* shouldResolveDomain(domain: string) {
const currentDomainName = yield select(getCurrentDomainName);
if (currentDomainName === domain) {
const currentDomainData = yield select(getCurrentDomainData);
if (currentDomainData) {
return false;
}
}
return true;
}
function* resolveDomain(): SagaIterator {
const requestChan = yield actionChannel(
TypeKeys.ENS_RESOLVE_DOMAIN_REQUESTED,
buffers.sliding(1)
);
while (true) {
const { payload }: ResolveDomainRequested = yield take(requestChan);
const { domain } = payload;
try {
const shouldResolve = yield call(shouldResolveDomain, domain);
if (!shouldResolve) {
yield put(resolveDomainCached({ domain }));
continue;
}
const node: INode = yield select(getNodeLib);
const result = yield race({
domainData: call(resolveDomainRequest, domain, node),
err: call(delay, 4000)
});
const { domainData } = result;
if (!domainData) {
throw Error();
}
const domainSuccessAction = resolveDomainSucceeded(domain, domainData);
yield put(domainSuccessAction);
yield;
} catch (e) {
const domainFailAction = resolveDomainFailed(domain, e);
yield put(domainFailAction);
yield put(showNotification('danger', e.message || 'Could not resolve ENS address', 5000));
}
}
}
export function* ens(): SagaIterator {
yield all([fork(resolveDomain)]);
}

View File

@ -0,0 +1,11 @@
import { select, apply, call } from 'redux-saga/effects';
import { INode } from 'libs/nodes/INode';
import { getNodeLib } from 'selectors/config';
import { SagaIterator } from 'redux-saga';
export function* makeEthCallAndDecode({ to, data, decoder }): SagaIterator {
const node: INode = yield select(getNodeLib);
const result: string = yield apply(node, node.sendCallRequest, [{ data, to }]);
const decodedResult = yield call(decoder, result);
return decodedResult;
}

View File

@ -0,0 +1 @@
export * from './ens';

View File

@ -0,0 +1,93 @@
import { IDomainData, NameState, getNameHash } from 'libs/ens';
import ENS from 'libs/ens/contracts';
import { SagaIterator } from 'redux-saga';
import { call } from 'redux-saga/effects';
import networkConfigs from 'libs/ens/networkConfigs';
import { makeEthCallAndDecode } from 'sagas/ens/helpers';
import ethUtil from 'ethereumjs-util';
const { main } = networkConfigs;
function* nameStateOwned({ deedAddress }: IDomainData<NameState.Owned>, nameHash: string) {
// Return the owner's address, and the resolved address if it exists
const { ownerAddress }: typeof ENS.deed.owner.outputType = yield call(makeEthCallAndDecode, {
to: deedAddress,
data: ENS.deed.owner.encodeInput(),
decoder: ENS.deed.owner.decodeOutput
});
const { resolverAddress }: typeof ENS.registry.resolver.outputType = yield call(
makeEthCallAndDecode,
{
to: main.registry,
decoder: ENS.registry.resolver.decodeOutput,
data: ENS.registry.resolver.encodeInput({
node: nameHash
})
}
);
let resolvedAddress = '0x0';
if (resolverAddress !== '0x0') {
const result: typeof ENS.resolver.addr.outputType = yield call(makeEthCallAndDecode, {
to: resolverAddress,
data: ENS.resolver.addr.encodeInput({ node: nameHash }),
decoder: ENS.resolver.addr.decodeOutput
});
resolvedAddress = result.ret;
}
return { ownerAddress, resolvedAddress };
}
function* nameStateReveal({ deedAddress }: IDomainData<NameState.Reveal>): SagaIterator {
const { ownerAddress }: typeof ENS.deed.owner.outputType = yield call(makeEthCallAndDecode, {
to: deedAddress,
data: ENS.deed.owner.encodeInput(),
decoder: ENS.deed.owner.decodeOutput
});
return ownerAddress;
}
interface IModeMap {
[x: string]: (
domainData: IDomainData<NameState>,
nameHash?: string,
hash?: Buffer
) =>
| {}
| { ownerAddress: string; resolvedAddress: string }
| { auctionCloseTime: string; revealBidTime: string };
}
const modeMap: IModeMap = {
[NameState.Open]: (_: IDomainData<NameState.Open>) => ({}),
[NameState.Auction]: (_: IDomainData<NameState.Auction>) => ({}),
[NameState.Owned]: nameStateOwned,
[NameState.Forbidden]: (_: IDomainData<NameState.Forbidden>) => ({}),
[NameState.Reveal]: nameStateReveal,
[NameState.NotYetAvailable]: (_: IDomainData<NameState.NotYetAvailable>) => ({})
};
export function* resolveDomainRequest(name: string): SagaIterator {
const hash = ethUtil.sha3(name);
const nameHash = getNameHash(`${name}.eth`);
const domainData: typeof ENS.auction.entries.outputType = yield call(makeEthCallAndDecode, {
to: main.public.ethAuction,
data: ENS.auction.entries.encodeInput({ _hash: hash }),
decoder: ENS.auction.entries.decodeOutput
});
const nameStateHandler = modeMap[domainData.mode];
const result = yield call(nameStateHandler, domainData, nameHash);
return {
name,
...domainData,
...result,
labelHash: hash.toString('hex'),
nameHash
};
}

View File

@ -12,9 +12,11 @@ import {
import { liteSend } from './swap/liteSend';
import { getBityRatesSaga, getShapeShiftRatesSaga, swapProviderSaga } from './swap/rates';
import wallet from './wallet';
import { ens } from './ens';
import { transaction } from './transaction';
export default {
ens,
liteSend,
configSaga,
postBityOrderSaga,

View File

@ -3,26 +3,43 @@ import { SetCurrentToAction } from 'actions/transaction/actionTypes/current';
import { setToField } from 'actions/transaction/actionCreators/fields';
import { setTokenTo } from 'actions/transaction/actionCreators/meta';
import { Address } from 'libs/units';
import { select, call, put, takeLatest } from 'redux-saga/effects';
import { select, call, put, takeLatest, take } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga';
import { isValidENSAddress, isValidETHAddress } from 'libs/validators';
import { TypeKeys } from 'actions/transaction/constants';
import { getResolvedAddress } from 'selectors/ens';
import { resolveDomainRequested, TypeKeys as ENSTypekeys } from 'actions/ens';
import { SetToFieldAction, SetTokenToMetaAction } from 'actions/transaction';
export function* setCurrentTo({ payload: raw }: SetCurrentToAction): SagaIterator {
const validAddress: boolean = yield call(isValidETHAddress, raw);
const validEns: boolean = yield call(isValidENSAddress, raw);
const etherTransaction: boolean = yield select(isEtherTransaction);
let value: Buffer | null = null;
let error: string | null = null;
if (validAddress) {
value = Address(raw);
} else if (validEns) {
// TODO: Resolve ENS on networks that support it, error on ones that don't
error = 'ENS is not supported yet';
yield call(setField, { value, raw });
const [domain] = raw.split('.');
yield put(resolveDomainRequested(domain));
yield take([
ENSTypekeys.ENS_RESOLVE_DOMAIN_FAILED,
ENSTypekeys.ENS_RESOLVE_DOMAIN_SUCCEEDED,
ENSTypekeys.ENS_RESOLVE_DOMAIN_CACHED
]);
const resolvedAddress: string | null = yield select(getResolvedAddress, true);
if (resolvedAddress) {
value = Address(resolvedAddress);
}
}
const payload = { raw, value, error };
yield call(setField, { value, raw });
}
export function* setField(payload: SetToFieldAction['payload'] | SetTokenToMetaAction['payload']) {
const etherTransaction: boolean = yield select(isEtherTransaction);
if (etherTransaction) {
yield put(setToField(payload));
} else {

View File

@ -1,7 +1,8 @@
$table-cell-padding: $space-sm;
$table-condensed-cell-padding: $space-xs;
$table-bg: transparent;
$table-bg-accent: #f9f9f9;
$table-bg-accent: #f5f5f5;
$table-bg-hover: $gray-lightest;
$table-bg-active: $table-bg-hover;
$table-border-color: #ddd;
$table-border-color: transparent;
$table-cell-padding: 0.75rem 1rem;

View File

@ -1,5 +1,54 @@
import { AppState } from 'reducers';
import { IOwnedDomainRequest, IBaseDomainRequest } from 'libs/ens';
import { REQUEST_STATES } from 'reducers/ens/domainRequests';
import { isCreationAddress } from 'libs/validators';
export function getEnsAddress(state: AppState, ensName: string): null | string {
return state.ens[ensName];
}
export const getEns = (state: AppState) => state.ens;
export const getCurrentDomainName = (state: AppState) => getEns(state).domainSelector.currentDomain;
export const getDomainRequests = (state: AppState) => getEns(state).domainRequests;
export const getCurrentDomainData = (state: AppState) => {
const currentDomain = getCurrentDomainName(state);
const domainRequests = getDomainRequests(state);
if (!currentDomain || !domainRequests[currentDomain] || domainRequests[currentDomain].error) {
return null;
}
const domainData = domainRequests[currentDomain].data || null;
return domainData;
};
export const getResolvedAddress = (state: AppState, noGenesisAddress: boolean = false) => {
const data = getCurrentDomainData(state);
if (!data) {
return null;
}
if (isOwned(data)) {
const { resolvedAddress } = data;
if (noGenesisAddress) {
return !isCreationAddress(resolvedAddress) ? resolvedAddress : null;
}
return data.resolvedAddress;
}
return null;
};
export const getResolvingDomain = (state: AppState) => {
const currentDomain = getCurrentDomainName(state);
const domainRequests = getDomainRequests(state);
if (!currentDomain || !domainRequests[currentDomain]) {
return null;
}
return domainRequests[currentDomain].state === REQUEST_STATES.pending;
};
const isOwned = (data: IBaseDomainRequest): data is IOwnedDomainRequest => {
return !!(data as IOwnedDomainRequest).ownerAddress;
};

View File

@ -13,7 +13,6 @@ interface ICurrentValue {
interface ICurrentTo {
raw: string;
value: Address | null;
error?: string | null;
}
const isEtherTransaction = (state: AppState) => {

View File

@ -369,7 +369,7 @@ declare module 'bn.js' {
* @description reduct
*/
modn(b: number): BN;
modn(b: number): number; //API consistency https://github.com/indutny/bn.js/pull/130
/**
* @description rounded division

21
spec/libs/ens.spec.ts Normal file
View File

@ -0,0 +1,21 @@
import * as ens from 'libs/ens';
// TODO: write tests for:
// ens.placeBid
// ens.unsealBid
// ens.resolveDomainRequest
describe('ENS', () => {
it('converts a domain name to a normalized Unicode', () => {
const data = ens.normalise('xn--s-qfa0g.de');
expect(data).toBe('süß.de');
});
it('converts a string to hexacedimal', () => {
const unicodeToHash = ens.getNameHash('Süß.de');
const asciiToHash = ens.getNameHash('xn--s-qfa0g.de');
expect(unicodeToHash && asciiToHash).toBe(
'0x26eb2a1d5e19a5d10e4a0001e7f3b22366f27d7203c6985b6b41fe65be107f8b'
);
});
});

View File

@ -15,23 +15,15 @@ describe('customTokens reducer', () => {
};
it('should handle CUSTOM_TOKEN_ADD', () => {
expect(
customTokens(undefined, customTokensActions.addCustomToken(token1))
).toEqual([token1]);
expect(customTokens(undefined, customTokensActions.addCustomToken(token1))).toEqual([token1]);
});
it('should handle CUSTOM_TOKEN_REMOVE', () => {
const state1 = customTokens(
undefined,
customTokensActions.addCustomToken(token1)
);
const state2 = customTokens(
state1,
customTokensActions.addCustomToken(token2)
);
const state1 = customTokens(undefined, customTokensActions.addCustomToken(token1));
const state2 = customTokens(state1, customTokensActions.addCustomToken(token2));
expect(
customTokens(state2, customTokensActions.removeCustomToken(token2.symbol))
).toEqual([token1]);
expect(customTokens(state2, customTokensActions.removeCustomToken(token2.symbol))).toEqual([
token1
]);
});
});

View File

@ -1,7 +1,4 @@
import {
deterministicWallets,
INITIAL_STATE
} from 'reducers/deterministicWallets';
import { deterministicWallets, INITIAL_STATE } from 'reducers/deterministicWallets';
import * as dWalletActions from 'actions/deterministicWallets';
import { TokenValue } from 'libs/units';
@ -23,10 +20,7 @@ describe('deterministicWallets reducer', () => {
it('should handle DW_SET_WALLETS', () => {
const wallets = [wallet];
expect(
deterministicWallets(
undefined,
dWalletActions.setDeterministicWallets(wallets)
)
deterministicWallets(undefined, dWalletActions.setDeterministicWallets(wallets))
).toEqual({
...INITIAL_STATE,
wallets
@ -35,12 +29,7 @@ describe('deterministicWallets reducer', () => {
it('should handle DW_SET_DESIRED_TOKEN', () => {
const desiredToken = 'OMG';
expect(
deterministicWallets(
undefined,
dWalletActions.setDesiredToken(desiredToken)
)
).toEqual({
expect(deterministicWallets(undefined, dWalletActions.setDesiredToken(desiredToken))).toEqual({
...INITIAL_STATE,
desiredToken
});
@ -56,10 +45,7 @@ describe('deterministicWallets reducer', () => {
address: 'wallet2'
};
const wallets = [wallet1, wallet2];
const state = deterministicWallets(
undefined,
dWalletActions.setDeterministicWallets(wallets)
);
const state = deterministicWallets(undefined, dWalletActions.setDeterministicWallets(wallets));
const wallet2Update = {
...wallet,
@ -69,10 +55,7 @@ describe('deterministicWallets reducer', () => {
};
expect(
deterministicWallets(
state,
dWalletActions.updateDeterministicWallet(wallet2Update)
)
deterministicWallets(state, dWalletActions.updateDeterministicWallet(wallet2Update))
).toEqual({
...INITIAL_STATE,
wallets: [wallet1, wallet2Update]

View File

@ -1,15 +1,16 @@
import { ens, INITIAL_STATE } from 'reducers/ens';
import { ens } from 'reducers/ens';
import * as ensActions from 'actions/ens';
import { createStore } from 'redux';
const store = createStore(ens);
const INITIAL_STATE = store.getState();
describe('customTokens reducer', () => {
it('should handle ENS_CACHE', () => {
it('handles resolveDomainRequested', () => {
const ensName = 'ensName';
const address = 'address';
expect(
ens(undefined, ensActions.cacheEnsAddress(ensName, address))
).toEqual({
expect(ens(undefined as any, ensActions.resolveDomainRequested(ensName))).toEqual({
...INITIAL_STATE,
[ensName]: address
domainRequests: { ensName: { state: 'PENDING' } },
domainSelector: { currentDomain: 'ensName' }
});
});
});

View File

@ -19,10 +19,7 @@ describe('customTokens reducer', () => {
const state2 = notifications(state1, notification2);
expect(
notifications(
state2,
notificationsActions.closeNotification(notification2.payload)
)
notifications(state2, notificationsActions.closeNotification(notification2.payload))
).toEqual([notification1.payload]);
});
});

View File

@ -15,9 +15,7 @@ describe('rates reducer', () => {
}
};
expect(
rates(undefined, ratesActions.fetchCCRatesSucceeded(fakeCCResp))
).toEqual({
expect(rates(undefined, ratesActions.fetchCCRatesSucceeded(fakeCCResp))).toEqual({
...INITIAL_STATE,
rates: {
...INITIAL_STATE.rates,
@ -27,8 +25,6 @@ describe('rates reducer', () => {
});
it('should handle RATES_FETCH_CC_FAILED', () => {
expect(rates(undefined, ratesActions.fetchCCRatesFailed())).toHaveProperty(
'ratesError'
);
expect(rates(undefined, ratesActions.fetchCCRatesFailed())).toHaveProperty('ratesError');
});
});

View File

@ -1,26 +1,14 @@
import { delay } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import { handleNotification } from 'sagas/notifications';
import {
ShowNotificationAction,
showNotification,
closeNotification
} from 'actions/notifications';
import { ShowNotificationAction, showNotification, closeNotification } from 'actions/notifications';
describe('handleNotification*', () => {
const level = 'success';
const msg = 'msg';
const duration = 10;
const notificationAction1: ShowNotificationAction = showNotification(
level,
msg,
duration
);
const notificationAction2: ShowNotificationAction = showNotification(
level,
msg,
0
);
const notificationAction1: ShowNotificationAction = showNotification(level, msg, duration);
const notificationAction2: ShowNotificationAction = showNotification(level, msg, 0);
const gen1 = handleNotification(notificationAction1);
const gen2 = handleNotification(notificationAction2);

View File

@ -1,55 +1,57 @@
import { isEtherTransaction } from 'selectors/transaction';
import { setToField } from 'actions/transaction/actionCreators/fields';
import { setTokenTo } from 'actions/transaction/actionCreators/meta';
import { Address } from 'libs/units';
import { select, call, put } from 'redux-saga/effects';
import { call, select, put } from 'redux-saga/effects';
import { isValidETHAddress, isValidENSAddress } from 'libs/validators';
import { setCurrentTo } from 'sagas/transaction/current/currentTo';
import { setCurrentTo, setField } from 'sagas/transaction/current/currentTo';
import { isEtherTransaction } from 'selectors/transaction';
import { cloneableGenerator } from 'redux-saga/utils';
import { setToField, setTokenTo } from 'actions/transaction';
const raw = '0xa';
const payload = {
raw,
value: Address(raw)
};
describe('setCurrentTo*', () => {
const raw = '0xa';
const action: any = {
payload: raw
};
const validAddress = true;
const validEns = false;
const etherTransaction = true;
const payload = {
raw,
value: Address(raw),
error: null
};
const gens: any = {};
gens.gen = cloneableGenerator(setCurrentTo)(action);
const gen = setCurrentTo(action);
it('should call isValidETHAddress', () => {
expect(gens.gen.next().value).toEqual(call(isValidETHAddress, raw));
expect(gen.next().value).toEqual(call(isValidETHAddress, raw));
});
it('should call isValidENSAddress', () => {
expect(gens.gen.next(validAddress).value).toEqual(call(isValidENSAddress, raw));
expect(gen.next(validAddress).value).toEqual(call(isValidENSAddress, raw));
});
it('should select isEtherTransaction', () => {
expect(gens.gen.next(validEns).value).toEqual(select(isEtherTransaction));
it('should call setField', () => {
expect(gen.next(validEns).value).toEqual(call(setField, payload));
});
it('should put setToField if etherTransaction', () => {
gens.ethTransaction = gens.gen.clone();
expect(gens.ethTransaction.next(etherTransaction).value).toEqual(put(setToField(payload)));
});
it('setToField should be done', () => {
expect(gens.ethTransaction.next().done).toEqual(true);
});
it('should put setTokenTo if !etherTransaction', () => {
expect(gens.gen.next(!etherTransaction).value).toEqual(put(setTokenTo(payload)));
});
it('setTokenTo should be done', () => {
expect(gens.gen.next().done).toEqual(true);
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
describe('setField', () => {
const etherTransaction = cloneableGenerator(setField)(payload);
it('should select etherTransaction', () => {
expect(etherTransaction.next().value).toEqual(select(isEtherTransaction));
});
it('should put setTokenTo field if its a token transaction ', () => {
const tokenTransaction = etherTransaction.clone();
expect(tokenTransaction.next(false).value).toEqual(put(setTokenTo(payload)));
expect(tokenTransaction.next().done).toBe(true);
});
it('should put setToField if its an etherTransaction', () => {
expect(etherTransaction.next(true).value).toEqual(put(setToField(payload)));
expect(etherTransaction.next().done).toBe(true);
});
});