ens-usernames/app/components/ens/nameLookup.js

384 lines
16 KiB
JavaScript
Raw Normal View History

import React, { Fragment, PureComponent } from 'react';
import web3 from 'web3';
import EmbarkJS from 'Embark/EmbarkJS';
import { connect } from 'react-redux';
import { actions as accountActions, getDefaultAccount } from '../../reducers/accounts';
2018-08-17 20:28:17 +00:00
import { hash } from 'eth-ens-namehash';
2018-08-27 17:23:07 +00:00
import { isNil } from 'lodash';
import Hidden from '@material-ui/core/Hidden';
2018-07-10 18:41:46 +00:00
import Typography from '@material-ui/core/Typography';
import UsernameRegistrar from 'Embark/contracts/UsernameRegistrar';
import ENSRegistry from 'Embark/contracts/ENSRegistry';
2018-08-16 19:14:43 +00:00
import { Button, Field, TextInput, MobileSearch, MobileButton, Card, Info, Text } from '../../ui/components'
2018-06-08 13:11:51 +00:00
import { IconCheck } from '../../ui/icons'
import { keyFromXY } from '../../utils/ecdsa';
2018-08-16 22:06:37 +00:00
import EditOptions from './EditOptions';
2018-08-17 19:46:59 +00:00
import ReleaseDomainAlert from './ReleaseDomain';
import theme from '../../ui/theme'
import { withFormik } from 'formik';
import PublicResolver from 'Embark/contracts/PublicResolver';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import RegisterSubDomain from '../ens/registerSubDomain';
import StatusLogo from '../../ui/icons/components/StatusLogo'
import EnsLogo from '../../ui/icons/logos/ens.png';
2018-07-13 19:08:21 +00:00
import { formatPrice } from '../ens/utils';
import CheckCircle from '../../ui/icons/components/baseline_check_circle_outline.png';
2018-09-24 15:15:43 +00:00
import WarningIcon from '../../ui/icons/svg/warning.svg';
2018-09-30 03:26:03 +00:00
const { getPrice, getReleaseTime, release } = UsernameRegistrar.methods;
2018-07-31 18:24:51 +00:00
import NotInterested from '@material-ui/icons/NotInterested';
2018-08-07 18:12:34 +00:00
import Face from '@material-ui/icons/Face';
2018-08-20 19:31:12 +00:00
import Copy from './copy';
import IDNANormalizer from 'idna-normalize';
2018-09-01 11:50:09 +00:00
import { nullAddress, getResolver } from './utils/domain';
const normalizer = new IDNANormalizer();
const invalidSuffix = '0000000000000000000000000000000000000000'
const validAddress = address => address != nullAddress;
const validStatusAddress = address => !address.includes(invalidSuffix);
const formatName = domainName => domainName.includes('.') ? normalizer.normalize(domainName) : normalizer.normalize(`${domainName}.stateofus.eth`);
const getDomain = fullDomain => formatName(fullDomain).split('.').slice(1).join('.');
const hashedDomain = domainName => hash(getDomain(domainName));
const registryIsOwner = address => address == UsernameRegistrar._address;
2018-08-17 20:28:17 +00:00
const { soliditySha3, fromWei } = web3.utils;
const cardStyle = {
width: '100%',
padding: '30px',
height: '425px'
}
const addressStyle = {
fontSize: '18px',
fontWeight: 400,
cursor: 'copy',
2018-06-08 13:11:51 +00:00
wordWrap: 'break-word',
}
const backButton = {
fontSize: '40px',
color: theme.accent,
cursor: 'pointer'
}
2018-09-01 12:22:06 +00:00
const validTimestamp = timestamp => Number(timestamp) > 99999999;
const generatePrettyDate = timestamp => new Date(timestamp * 1000).toDateString();
2018-06-19 14:12:49 +00:00
2018-07-31 19:59:04 +00:00
const DisplayBox = ({ displayType, pubKey }) => (
2018-09-27 20:32:06 +00:00
<div>
<div style={{ fontSize: '14px', color: '#939BA1', margin: '0 1em' }}>{displayType}</div>
<div style={{ border: '1px solid #EEF2F5', borderRadius: '8px', margin: '0.5 1em 1em', display: 'flex', flexDirection: 'column', justifyContent: 'space-around', minHeight: '4em' }}>
<div style={{ margin: '3%', wordBreak: 'break-word' }}>
<Typography type='body1'>{pubKey}</Typography>
</div>
2018-07-31 19:59:04 +00:00
</div>
</div>
);
2018-10-01 13:47:54 +00:00
const MobileAddressDisplay = ({ domainName, address, statusAccount, releaseTime, defaultAccount, isOwner, edit, onSubmit, handleChange, values, handleSubmit }) => (
<Fragment>
<LookupForm {...{ handleSubmit, values, handleChange }} justSearch />
<Info background={isOwner ? '#44D058' : '#000000'} style={{ margin: '0.4em', boxShadow: '0px 6px 10px rgba(0, 0, 0, 0.2)' }}>
<Typography variant="title" style={
{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'space-evenly', height: '4em', color: '#ffffff', textAlign: 'center', margin: '10%' }
}>
{isOwner ? <Face style={{ marginBottom: '0.5em', fontSize: '2em' }} /> : <NotInterested style={{ marginBottom: '0.5em', fontSize: '2em' }}/>}
<b>{formatName(domainName)}</b>
<div style={{ fontWeight: 300, fontSize: '15px', marginTop: '10px' }}>
{isOwner
? edit ? 'Edit Contact Code' : 'You own this ENS name'
: 'unavailable'}
</div>
2018-09-28 18:20:58 +00:00
</Typography>
2018-10-01 13:47:54 +00:00
</Info>
<Typography type='subheading' style={{ textAlign: 'center', fontSize: '17px', fontWeight: '500', margin: '1.5em 0 0.3em 0' }}>
Registered {validTimestamp(releaseTime)}
</Typography>
<Typography type='body2' style={{ textAlign: 'center', margin: 10 }}>
{edit
? 'The contact code connects the domain with a unique Status account'
: validAddress(address) ? 'to the addresses below' : 'Click \'Edit\' to add a valid address and contact code'}
</Typography>
{edit && <RegisterSubDomain
subDomain={domainName}
domainName="stateofus.eth"
domainPrice="DO NOT SHOW"
editAccount={true}
preRegisteredCallback={onSubmit}
registeredCallbackFn={console.log} />}
{!edit && <DisplayBox displayType='Your wallet address' pubKey={address} />}
{!edit && validStatusAddress(statusAccount) && <DisplayBox displayType='Your contact code' pubKey={statusAccount} />}
</Fragment>
)
class RenderAddresses extends PureComponent {
state = { copied: false, editMenu: false, editAction: false }
render() {
2018-09-30 03:26:03 +00:00
const { domainName, address, statusAccount, releaseTime, defaultAccount, ownerAddress, setStatus, registryOwnsDomain } = this.props
2018-08-22 14:39:53 +00:00
const { copied, editMenu, editAction, submitted } = this.state
const markCopied = (v) => { this.setState({ copied: v }) }
const isCopied = address => address == copied;
const renderCopied = address => isCopied(address) && <span style={{ color: theme.positive }}><IconCheck/>Copied!</span>;
2018-08-17 16:14:09 +00:00
const onClose = value => { this.setState({ editAction: value, editMenu: false }) }
const onClickEdit = () => { validAddress(address) ? this.setState({ editMenu: true }) : this.setState({ editAction: 'edit' }) }
const isOwner = defaultAccount === ownerAddress;
2018-08-17 20:28:17 +00:00
const closeReleaseAlert = value => {
2018-08-27 17:23:07 +00:00
if (!isNil(value)) {
2018-08-22 19:20:34 +00:00
this.setState({ submitted: true })
2018-08-17 20:28:17 +00:00
release(
soliditySha3(domainName)
2018-08-17 20:28:17 +00:00
)
.send()
2018-08-27 17:23:07 +00:00
} else {
this.setState({ editAction: null })
2018-08-17 20:28:17 +00:00
}
}
return (
2018-07-31 18:24:51 +00:00
<Fragment>
<Hidden mdDown>
<div style={{ display: 'flex', flexDirection: 'column', margin: 50 }}>
2018-09-30 03:26:03 +00:00
<Info.Action title="Click to copy"><b>{formatName(domainName)}</b>{releaseTime && <i> (Expires {generatePrettyDate(releaseTime)})</i>} Resolves To:</Info.Action>
2018-07-31 18:24:51 +00:00
{address && <Text style={{ marginTop: '1em' }}>Ethereum Address {renderCopied(address)}</Text>}
<CopyToClipboard text={address} onCopy={markCopied}>
<div style={addressStyle}>{address}</div>
</CopyToClipboard>
{validStatusAddress(statusAccount) && <Text style={{ marginTop: '1em' }}>Status Address {renderCopied(statusAccount)}</Text>}
{validStatusAddress(statusAccount) && <CopyToClipboard text={statusAccount} onCopy={markCopied}>
<div style={{ ...addressStyle, color: isCopied ? theme.primary : null }}>{statusAccount}</div>
</CopyToClipboard>}
</div>
</Hidden>
<Hidden mdUp>
2018-08-22 19:20:34 +00:00
{submitted ? <TransactionComplete type={editAction} setStatus={setStatus} /> : <MobileAddressDisplay {...this.props} isOwner={isOwner} edit={editAction === 'edit'} onSubmit={() => { this.setState({ submitted: true}) }}/>}
2018-09-27 20:32:06 +00:00
{isOwner && !editAction && <MobileButton text="Edit" style={{ margin: 'auto', display: 'block' }} onClick={onClickEdit}/>}
<EditOptions open={editMenu} onClose={onClose} />
2018-08-22 19:20:34 +00:00
<ReleaseDomainAlert open={editAction === 'release' && !submitted} handleClose={closeReleaseAlert} />
2018-07-31 18:24:51 +00:00
</Hidden>
</Fragment>
)
}
}
const RegisterInfoCard = ({ formattedDomain, domainPrice, registryOwnsDomain }) => (
2018-07-10 18:41:46 +00:00
<Fragment>
<Hidden mdDown>
<Info.Action title="No address is associated with this domain">
2018-07-10 19:23:21 +00:00
<span style={{ color: theme.accent }}>{formattedDomain.toLowerCase()}</span> can be registered for {!!domainPrice && formatPrice(fromWei(domainPrice))} SNT
2018-07-10 18:41:46 +00:00
</Info.Action>
</Hidden>
<Hidden mdUp>
2018-07-10 19:23:21 +00:00
<Info background="#415be3" style={{ margin: '0.4em' }}>
2018-07-10 18:41:46 +00:00
<Typography variant="title" style={
{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'space-evenly', height: '4em', color: '#ffffff', textAlign: 'center', margin: '10%' }
}>
2018-07-13 19:08:21 +00:00
<img src={CheckCircle} style={{ maxWidth: '2.5em', marginBottom: '0.5em' }} />
2018-07-10 19:23:21 +00:00
<b>{formattedDomain.toLowerCase()}</b>
2018-09-27 20:32:06 +00:00
<div style={{ fontWeight: 300, fontSize: '15px' }}>
available
2018-07-10 18:41:46 +00:00
</div>
</Typography>
</Info>
</Hidden>
2018-07-10 19:23:21 +00:00
<Hidden mdUp>
2018-09-27 20:32:06 +00:00
<Typography style={{ textAlign: 'center', fontSize: '17px', fontWeight: '500', margin: '1.5em 0 0.3em 0' }}>
{!!domainPrice && formatPrice(fromWei(domainPrice))} SNT to register
</Typography>
2018-07-11 18:44:32 +00:00
<Typography style={{ textAlign: 'center', padding: '1.5em' }}>
{registryOwnsDomain ?
2018-09-27 20:32:06 +00:00
'Add your contact code to use your name in Status chat.' :
'This domain is not owned by the registry'}
2018-07-10 19:23:21 +00:00
</Typography>
</Hidden>
2018-07-10 18:41:46 +00:00
</Fragment>
)
2018-08-22 19:20:34 +00:00
const TransactionComplete = ({ type, setStatus }) => (
2018-08-20 19:31:12 +00:00
<div style={{ textAlign: 'center', margin: '40% 15 10' }}>
<Typography variant="title" style={{ marginBottom: '1rem' }}>
{Copy[type]['title']['sub']}<br/>
{Copy[type]['title']['body']}
</Typography>
<Typography variant="subheading" style={{ color: '#939BA1' }}>
{Copy[type]['subheading']}
</Typography>
2018-08-22 19:20:34 +00:00
<MobileButton text="Main Page" style={{ marginTop: '12rem' }} onClick={() => { setStatus(null) } } />
2018-08-20 19:31:12 +00:00
</div>
);
class Register extends PureComponent {
state = { domainPrice: null };
componentDidMount() {
const { domainName } = this.props;
getPrice()
.call()
.then((res) => { this.setState({ domainPrice: res })});
}
onRegistered = (address, statusAccount) => {
const { domainPrice } = this.state;
const { subtractFromBalance } = this.props;
subtractFromBalance(domainPrice);
this.setState({ registered: { address, statusAccount } });
}
render() {
2018-09-01 11:50:09 +00:00
const { domainName, setStatus, style, registryOwnsDomain, ownerAddress, defaultAccount } = this.props;
2018-08-20 19:31:12 +00:00
const { domainPrice, registered, submitted } = this.state;
const formattedDomain = formatName(domainName);
2018-08-20 19:31:12 +00:00
const formattedDomainArray = formattedDomain.split('.');
2018-09-01 11:50:09 +00:00
const isOwner = defaultAccount === ownerAddress;
return (
2018-07-13 19:29:11 +00:00
<div style={style}>
2018-08-20 19:31:12 +00:00
{!registered && !submitted ?
<Fragment>
<RegisterInfoCard {...{ formattedDomain, domainPrice, registryOwnsDomain }}/>
{registryOwnsDomain &&
<RegisterSubDomain
subDomain={formattedDomainArray[0]}
domainName={formattedDomainArray.slice(1).join('.')}
domainPrice={domainPrice}
2018-08-20 19:31:12 +00:00
preRegisteredCallback={() => { this.setState({ submitted: true }) }}
registeredCallbackFn={this.onRegistered} />}
</Fragment> :
2018-08-22 19:20:34 +00:00
submitted && !registered ? <TransactionComplete type="registered" setStatus={setStatus} /> : <RenderAddresses {...this.props} address={registered.address} statusAccount={registered.statusAccount} />}
2018-07-13 19:29:11 +00:00
</div>
)
}
}
const mapDispatchToProps = dispatch => ({
subtractFromBalance(amount) {
dispatch(accountActions.subtractfromSntTokenBalance(amount));
},
});
const mapStateToProps = state => ({
defaultAccount: getDefaultAccount(state)
})
const ConnectedRegister = connect(mapStateToProps, mapDispatchToProps)(Register);
const DisplayAddress = connect(mapStateToProps)((props) => (
<Fragment>
{validAddress(props.address) || props.defaultAccount === props.ownerAddress ?
<RenderAddresses {...props} />
:
2018-07-10 18:41:46 +00:00
<Hidden mdUp>
<Info.Action title="No address is associated with this domain">
2018-08-10 19:15:39 +00:00
{props.domainName}
2018-07-10 18:41:46 +00:00
</Info.Action>
</Hidden>
}
</Fragment>
))
const LookupForm = ({ handleSubmit, values, handleChange, justSearch }) => (
<Fragment>
<form onSubmit={handleSubmit} onBlur={handleSubmit} >
2018-07-10 18:41:46 +00:00
<Hidden mdDown>
2018-08-18 20:34:45 +00:00
<Field label="Enter Domain or Status Name" style={{ margin: 50 }}>
<TextInput
value={values.domainName}
name="domainName"
onChange={handleChange}
wide
required />
</Field>
</Hidden>
2018-07-10 18:41:46 +00:00
<Hidden mdUp>
2018-07-13 17:59:18 +00:00
<MobileSearch
search
name="domainName"
2018-09-24 15:15:43 +00:00
placeholder='Search for available name'
2018-07-13 17:59:18 +00:00
value={values.domainName}
onChange={handleChange}
2018-07-13 17:59:18 +00:00
required
wide />
2018-09-24 15:15:43 +00:00
{!justSearch &&
<div style={{ textAlign: 'center', marginTop: '40vh' }}>
<img style={{ display: 'block', margin: '0 auto 15px' }} src={WarningIcon} />
Names are made with<br/>letters and numbers only
</div>
}
</Hidden>
2018-07-10 18:41:46 +00:00
<Hidden mdDown>
2018-08-18 20:34:45 +00:00
<Button mode="strong" type="submit" style={{ marginLeft: '3%', maxWidth: '95%' }} wide>
Lookup Address
</Button>
</Hidden>
</form>
</Fragment>
)
const InnerForm = ({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
status,
setStatus,
defaultAccount
}) => (
2018-07-13 19:29:11 +00:00
<div>
2018-07-10 18:41:46 +00:00
<Hidden mdDown>
2018-08-18 20:34:45 +00:00
<span style={{ display: 'flex', justifyContent: 'space-evenly', margin: '50 0 10 0' }}>
<StatusLogo />
<img style={{ maxWidth: '150px', alignSelf: 'center' }} src={EnsLogo} alt="Ens Logo"/>
</span>
2018-09-28 21:33:18 +00:00
</Hidden>
{!status
? <LookupForm {...{ handleSubmit, values, handleChange }} />
: validAddress(status.address) || defaultAccount === status.ownerAddress ?
<DisplayAddress
2018-09-28 18:20:58 +00:00
{...{ handleSubmit, values, handleChange }}
2018-09-28 21:33:18 +00:00
domainName={status.resolvedDomainName}
address={status.address}
statusAccount={status.statusAccount}
2018-09-30 03:26:03 +00:00
releaseTime={status.releaseTime}
ownerAddress={status.ownerAddress}
2018-08-29 16:01:32 +00:00
registryOwnsDomain={status.registryOwnsDomain}
setStatus={setStatus} /> :
2018-09-28 18:20:58 +00:00
<div>
<LookupForm {...{ handleSubmit, values, handleChange }} justSearch />
<ConnectedRegister
style={{ position: 'relative' }}
setStatus={setStatus}
registryOwnsDomain={status.registryOwnsDomain}
ownerAddress={status.ownerAddress}
2018-09-28 21:33:18 +00:00
domainName={status.resolvedDomainName} />
2018-09-28 18:20:58 +00:00
</div>
}
</div>
)
const NameLookup = withFormik({
mapPropsToValues: props => ({ domainName: '' }),
async handleSubmit(values, { status, setSubmitting, setStatus }) {
const { domainName } = values;
const { methods: { owner, resolver } } = ENSRegistry;
const lookupHash = hash(formatName(domainName));
2018-09-01 11:50:09 +00:00
const resolverContract = await getResolver(lookupHash);
const { addr, pubkey } = resolverContract.methods;
const address = addr(lookupHash).call();
const keys = pubkey(lookupHash).call();
const ownerAddress = owner(lookupHash).call();
2018-08-28 19:46:00 +00:00
const suffixOwner = owner(hash(getDomain(domainName))).call();
2018-09-30 03:26:03 +00:00
const releaseTime = getReleaseTime(lookupHash).call();
Promise.all([address, keys, ownerAddress, releaseTime, suffixOwner])
.then(([ address, keys, ownerAddress, releaseTime, suffixOwner ]) => {
const statusAccount = keyFromXY(keys[0], keys[1]);
2018-08-28 19:46:00 +00:00
const registryOwnsDomain = registryIsOwner(suffixOwner)
2018-09-28 21:33:18 +00:00
const resolvedDomainName = domainName;
setStatus({ address, statusAccount, releaseTime, ownerAddress, registryOwnsDomain, resolvedDomainName });
})
}
})(InnerForm)
export default connect(mapStateToProps)(NameLookup);