registry dapp: cleanup, support reverse entries (#3933)

* style fixes 

* registry dapp: show reverse events

* registry dapp: actions & reducers for isTestnet

* registry dapp: make Hash & Address components

* registry dapp: code style 

* registry dapp: bugfixes 🐛

* registry dapp: postTx helper

* registry dapp: refactor reducers

* registry dapp: use react-redux

* registry dapp: actions & reducers for reverse lookup

* registry dapp: reverse lookup component

* registry dapp: connect Address to redux

* registry dapp: de-DRY recordTypeSelect

In preparation for the next commit.

* registry dapp: support reverse lookup

* registry dapp: render reverse events

* registry dapp: show tx sender, add key prop

* registry dapp: link accounts to etherscan as well

* registry dapp: address style grumbles 💄

* registry dapp: address style grumbles 💄
This commit is contained in:
Jannis Redmann 2016-12-27 11:01:16 +01:00 committed by Gav Wood
parent 1ffc6ac58c
commit 002e8b00d4
33 changed files with 1177 additions and 351 deletions

View File

@ -15,22 +15,26 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import IconMenu from 'material-ui/IconMenu'; import IconMenu from 'material-ui/IconMenu';
import IconButton from 'material-ui/IconButton/IconButton'; import IconButton from 'material-ui/IconButton/IconButton';
import AccountIcon from 'material-ui/svg-icons/action/account-circle'; import AccountIcon from 'material-ui/svg-icons/action/account-circle';
import MenuItem from 'material-ui/MenuItem'; import MenuItem from 'material-ui/MenuItem';
import IdentityIcon from '../IdentityIcon'; import IdentityIcon from '../IdentityIcon';
import renderAddress from '../ui/address'; import Address from '../ui/address';
import { select } from './actions';
import styles from './accounts.css'; import styles from './accounts.css';
export default class Accounts extends Component { class Accounts extends Component {
static propTypes = { static propTypes = {
actions: PropTypes.object.isRequired,
all: PropTypes.object.isRequired, all: PropTypes.object.isRequired,
selected: PropTypes.object selected: PropTypes.object,
select: PropTypes.func.isRequired
} }
render () { render () {
@ -41,8 +45,17 @@ export default class Accounts extends Component {
const accountsButton = ( const accountsButton = (
<IconButton className={ styles.button }> <IconButton className={ styles.button }>
{ selected { selected
? (<IdentityIcon className={ styles.icon } address={ selected.address } />) ? (
: (<AccountIcon className={ styles.icon } color='white' />) <IdentityIcon
className={ styles.icon }
address={ selected.address }
/>
) : (
<AccountIcon
className={ styles.icon }
color='white'
/>
)
} }
</IconButton>); </IconButton>);
@ -61,20 +74,27 @@ export default class Accounts extends Component {
} }
renderAccount = (account) => { renderAccount = (account) => {
const { all, selected } = this.props; const { selected } = this.props;
const isSelected = selected && selected.address === account.address; const isSelected = selected && selected.address === account.address;
return ( return (
<MenuItem <MenuItem
key={ account.address } value={ account.address } key={ account.address }
checked={ isSelected } insetChildren={ !isSelected } value={ account.address }
checked={ isSelected }
insetChildren={ !isSelected }
> >
{ renderAddress(account.address, all, {}) } <Address address={ account.address } />
</MenuItem> </MenuItem>
); );
}; };
onAccountSelect = (e, address) => { onAccountSelect = (e, address) => {
this.props.actions.select(address); this.props.select(address);
}; };
} }
const mapStateToProps = (state) => state.accounts;
const mapDispatchToProps = (dispatch) => bindActionCreators({ select }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Accounts);

View File

@ -30,6 +30,7 @@ import Events from '../Events';
import Lookup from '../Lookup'; import Lookup from '../Lookup';
import Names from '../Names'; import Names from '../Names';
import Records from '../Records'; import Records from '../Records';
import Reverse from '../Reverse';
export default class Application extends Component { export default class Application extends Component {
static childContextTypes = { static childContextTypes = {
@ -42,26 +43,14 @@ export default class Application extends Component {
} }
static propTypes = { static propTypes = {
actions: PropTypes.object.isRequired,
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired, contract: nullableProptype(PropTypes.object).isRequired,
contract: nullableProptype(PropTypes.object.isRequired), fee: nullableProptype(PropTypes.object).isRequired
fee: nullableProptype(PropTypes.object.isRequired),
lookup: PropTypes.object.isRequired,
events: PropTypes.object.isRequired,
names: PropTypes.object.isRequired,
records: PropTypes.object.isRequired
}; };
render () { render () {
const { api } = window.parity; const { api } = window.parity;
const { const { contract, fee } = this.props;
actions,
accounts, contacts,
contract, fee,
lookup,
events
} = this.props;
let warning = null; let warning = null;
return ( return (
@ -69,13 +58,13 @@ export default class Application extends Component {
{ warning } { warning }
<div className={ styles.header }> <div className={ styles.header }>
<h1>RΞgistry</h1> <h1>RΞgistry</h1>
<Accounts { ...accounts } actions={ actions.accounts } /> <Accounts />
</div> </div>
{ contract && fee ? ( { contract && fee ? (
<div> <div>
<Lookup { ...lookup } accounts={ accounts.all } contacts={ contacts } actions={ actions.lookup } /> <Lookup />
{ this.renderActions() } { this.renderActions() }
<Events { ...events } accounts={ accounts.all } contacts={ contacts } actions={ actions.events } /> <Events />
<div className={ styles.warning }> <div className={ styles.warning }>
WARNING: The name registry is experimental. Please ensure that you understand the risks, benefits & consequences of registering a name before doing so. A non-refundable fee of { api.util.fromWei(fee).toFormat(3) }<small>ETH</small> is required for all registrations. WARNING: The name registry is experimental. Please ensure that you understand the risks, benefits & consequences of registering a name before doing so. A non-refundable fee of { api.util.fromWei(fee).toFormat(3) }<small>ETH</small> is required for all registrations.
</div> </div>
@ -88,15 +77,7 @@ export default class Application extends Component {
} }
renderActions () { renderActions () {
const { const hasAccount = !!this.props.accounts.selected;
actions,
accounts,
fee,
names,
records
} = this.props;
const hasAccount = !!accounts.selected;
if (!hasAccount) { if (!hasAccount) {
return ( return (
@ -111,8 +92,9 @@ export default class Application extends Component {
return ( return (
<div> <div>
<Names { ...names } fee={ fee } actions={ actions.names } /> <Names />
<Records { ...records } actions={ actions.records } /> <Records />
<Reverse />
</div> </div>
); );
} }

View File

@ -37,6 +37,7 @@ class Container extends Component {
componentDidMount () { componentDidMount () {
Promise.all([ Promise.all([
this.props.actions.fetchIsTestnet(),
this.props.actions.addresses.fetch(), this.props.actions.addresses.fetch(),
this.props.actions.fetchContract() this.props.actions.fetchContract()
]).then(() => { ]).then(() => {

View File

@ -42,8 +42,11 @@ export const subscribe = (name, from = 0, to = 'pending') =>
} }
events.forEach((e) => { events.forEach((e) => {
api.eth.getBlockByNumber(e.blockNumber) Promise.all([
.then((block) => { api.eth.getBlockByNumber(e.blockNumber),
api.eth.getTransactionByHash(e.transactionHash)
])
.then(([block, tx]) => {
const data = { const data = {
type: name, type: name,
key: '' + e.transactionHash + e.logIndex, key: '' + e.transactionHash + e.logIndex,
@ -51,6 +54,8 @@ export const subscribe = (name, from = 0, to = 'pending') =>
block: e.blockNumber, block: e.blockNumber,
index: e.logIndex, index: e.logIndex,
transaction: e.transactionHash, transaction: e.transactionHash,
from: tx.from,
to: tx.to,
parameters: e.params, parameters: e.params,
timestamp: block.timestamp timestamp: block.timestamp
}; };

View File

@ -49,3 +49,9 @@
.eventsList td { .eventsList td {
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
} }
.inline {
display: inline-block;
width: auto;
margin-right: 1em;
}

View File

@ -15,13 +15,17 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Card, CardHeader, CardActions, CardText } from 'material-ui/Card'; import { Card, CardHeader, CardActions, CardText } from 'material-ui/Card';
import Toggle from 'material-ui/Toggle'; import Toggle from 'material-ui/Toggle';
import moment from 'moment'; import moment from 'moment';
import { bytesToHex } from '../parity'; import { bytesToHex } from '../parity';
import renderHash from '../ui/hash'; import Hash from '../ui/hash';
import renderAddress from '../ui/address'; import Address from '../ui/address';
import { subscribe, unsubscribe } from './actions';
import styles from './events.css'; import styles from './events.css';
const inlineButton = { const inlineButton = {
@ -42,21 +46,31 @@ const renderStatus = (timestamp, isPending) => {
); );
}; };
const renderEvent = (classNames, verb) => (e, accounts, contacts) => { const renderEvent = (classNames, verb) => (e) => {
const classes = e.state === 'pending' const classes = e.state === 'pending'
? classNames + ' ' + styles.pending : classNames; ? classNames + ' ' + styles.pending : classNames;
return ( return (
<tr key={ e.key } className={ classes }> <tr key={ e.key } className={ classes }>
<td>{ renderAddress(e.parameters.owner.value, accounts, contacts) }</td> <td>
<td><abbr title={ e.transaction }>{ verb }</abbr></td> <Address address={ e.parameters.owner.value } />
<td><code>{ renderHash(bytesToHex(e.parameters.name.value)) }</code></td> </td>
<td>{ renderStatus(e.timestamp, e.state === 'pending') }</td> <td>
<abbr title={ e.transaction }>{ verb }</abbr>
</td>
<td>
<code>
<Hash hash={ bytesToHex(e.parameters.name.value) } />
</code>
</td>
<td>
{ renderStatus(e.timestamp, e.state === 'pending') }
</td>
</tr> </tr>
); );
}; };
const renderDataChanged = (e, accounts, contacts) => { const renderDataChanged = (e) => {
let classNames = styles.dataChanged; let classNames = styles.dataChanged;
if (e.state === 'pending') { if (e.state === 'pending') {
classNames += ' ' + styles.pending; classNames += ' ' + styles.pending;
@ -64,12 +78,61 @@ const renderDataChanged = (e, accounts, contacts) => {
return ( return (
<tr key={ e.key } className={ classNames }> <tr key={ e.key } className={ classNames }>
<td>{ renderAddress(e.parameters.owner.value, accounts, contacts) }</td>
<td><abbr title={ e.transaction }>updated</abbr></td>
<td> <td>
key <code>{ new Buffer(e.parameters.plainKey.value).toString('utf8') }</code> of <code>{ renderHash(bytesToHex(e.parameters.name.value)) }</code> <Address address={ e.parameters.owner.value } />
</td>
<td>
<abbr title={ e.transaction }>updated</abbr>
</td>
<td>
{ 'key ' }
<code>
{ new Buffer(e.parameters.plainKey.value).toString('utf8') }
</code>
{ 'of ' }
<code>
<Hash hash={ bytesToHex(e.parameters.name.value) } />
</code>
</td>
<td>
{ renderStatus(e.timestamp, e.state === 'pending') }
</td>
</tr>
);
};
const renderReverse = (e) => {
const verb = ({
ReverseProposed: 'proposed',
ReverseConfirmed: 'confirmed',
ReverseRemoved: 'removed'
})[e.type];
if (!verb) {
return null;
}
const classes = [ styles.reverse ];
if (e.state === 'pending') {
classes.push(styles.pending);
}
// TODO: `name` is an indexed param, cannot display as plain text
return (
<tr key={ e.key } className={ classes.join(' ') }>
<td>
<Address address={ e.from } />
</td>
<td>{ verb }</td>
<td>
{ 'name ' }
<code key='name'>{ bytesToHex(e.parameters.name.value) }</code>
{ ' for ' }
<Address key='reverse' address={ e.parameters.reverse.value } />
</td>
<td>
{ renderStatus(e.timestamp, e.state === 'pending') }
</td> </td>
<td>{ renderStatus(e.timestamp, e.state === 'pending') }</td>
</tr> </tr>
); );
}; };
@ -77,22 +140,25 @@ const renderDataChanged = (e, accounts, contacts) => {
const eventTypes = { const eventTypes = {
Reserved: renderEvent(styles.reserved, 'reserved'), Reserved: renderEvent(styles.reserved, 'reserved'),
Dropped: renderEvent(styles.dropped, 'dropped'), Dropped: renderEvent(styles.dropped, 'dropped'),
DataChanged: renderDataChanged DataChanged: renderDataChanged,
ReverseProposed: renderReverse,
ReverseConfirmed: renderReverse,
ReverseRemoved: renderReverse
}; };
export default class Events extends Component { class Events extends Component {
static propTypes = { static propTypes = {
actions: PropTypes.object.isRequired,
subscriptions: PropTypes.object.isRequired,
pending: PropTypes.object.isRequired,
events: PropTypes.array.isRequired, events: PropTypes.array.isRequired,
accounts: PropTypes.object.isRequired, pending: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired subscriptions: PropTypes.object.isRequired,
subscribe: PropTypes.func.isRequired,
unsubscribe: PropTypes.func.isRequired
} }
render () { render () {
const { subscriptions, pending, accounts, contacts } = this.props; const { subscriptions, pending } = this.props;
const eventsObject = this.props.events const eventsObject = this.props.events
.filter((e) => eventTypes[e.type]) .filter((e) => eventTypes[e.type])
@ -122,7 +188,16 @@ export default class Events extends Component {
return evB.timestamp - evA.timestamp; return evB.timestamp - evA.timestamp;
}) })
.map((e) => eventTypes[e.type](e, accounts, contacts)); .map((e) => eventTypes[e.type](e));
const reverseToggled =
subscriptions.ReverseProposed !== null &&
subscriptions.ReverseConfirmed !== null &&
subscriptions.ReverseRemoved !== null;
const reverseDisabled =
pending.ReverseProposed ||
pending.ReverseConfirmed ||
pending.ReverseRemoved;
return ( return (
<Card className={ styles.events }> <Card className={ styles.events }>
@ -149,6 +224,13 @@ export default class Events extends Component {
onToggle={ this.onDataChangedToggle } onToggle={ this.onDataChangedToggle }
style={ inlineButton } style={ inlineButton }
/> />
<Toggle
label='Reverse Lookup'
toggled={ reverseToggled }
disabled={ reverseDisabled }
onToggle={ this.onReverseToggle }
style={ inlineButton }
/>
</CardActions> </CardActions>
<CardText> <CardText>
<table className={ styles.eventsList }> <table className={ styles.eventsList }>
@ -162,33 +244,59 @@ export default class Events extends Component {
} }
onReservedToggle = (e, isToggled) => { onReservedToggle = (e, isToggled) => {
const { pending, subscriptions, actions } = this.props; const { pending, subscriptions, subscribe, unsubscribe } = this.props;
if (!pending.Reserved) { if (!pending.Reserved) {
if (isToggled && subscriptions.Reserved === null) { if (isToggled && subscriptions.Reserved === null) {
actions.subscribe('Reserved'); subscribe('Reserved');
} else if (!isToggled && subscriptions.Reserved !== null) { } else if (!isToggled && subscriptions.Reserved !== null) {
actions.unsubscribe('Reserved'); unsubscribe('Reserved');
} }
} }
}; };
onDroppedToggle = (e, isToggled) => { onDroppedToggle = (e, isToggled) => {
const { pending, subscriptions, actions } = this.props; const { pending, subscriptions, subscribe, unsubscribe } = this.props;
if (!pending.Dropped) { if (!pending.Dropped) {
if (isToggled && subscriptions.Dropped === null) { if (isToggled && subscriptions.Dropped === null) {
actions.subscribe('Dropped'); subscribe('Dropped');
} else if (!isToggled && subscriptions.Dropped !== null) { } else if (!isToggled && subscriptions.Dropped !== null) {
actions.unsubscribe('Dropped'); unsubscribe('Dropped');
} }
} }
}; };
onDataChangedToggle = (e, isToggled) => { onDataChangedToggle = (e, isToggled) => {
const { pending, subscriptions, actions } = this.props; const { pending, subscriptions, subscribe, unsubscribe } = this.props;
if (!pending.DataChanged) { if (!pending.DataChanged) {
if (isToggled && subscriptions.DataChanged === null) { if (isToggled && subscriptions.DataChanged === null) {
actions.subscribe('DataChanged'); subscribe('DataChanged');
} else if (!isToggled && subscriptions.DataChanged !== null) { } else if (!isToggled && subscriptions.DataChanged !== null) {
actions.unsubscribe('DataChanged'); unsubscribe('DataChanged');
}
}
};
onReverseToggle = (e, isToggled) => {
const { pending, subscriptions, subscribe, unsubscribe } = this.props;
for (let e of ['ReverseProposed', 'ReverseConfirmed', 'ReverseRemoved']) {
if (pending[e]) {
continue;
}
if (isToggled && subscriptions[e] === null) {
subscribe(e);
} else if (!isToggled && subscriptions[e] !== null) {
unsubscribe(e);
} }
} }
}; };
} }
const mapStateToProps = (state) => state.events;
const mapDispatchToProps = (dispatch) => bindActionCreators({ subscribe, unsubscribe }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Events);

View File

@ -18,12 +18,18 @@ const initialState = {
subscriptions: { subscriptions: {
Reserved: null, Reserved: null,
Dropped: null, Dropped: null,
DataChanged: null DataChanged: null,
ReverseProposed: null,
ReverseConfirmed: null,
ReverseRemoved: null
}, },
pending: { pending: {
Reserved: false, Reserved: false,
Dropped: false, Dropped: false,
DataChanged: false DataChanged: false,
ReverseProposed: false,
ReverseConfirmed: false,
ReverseRemoved: false
}, },
events: [] events: []
}; };

View File

@ -18,15 +18,15 @@ import { sha3 } from '../parity.js';
export const clear = () => ({ type: 'lookup clear' }); export const clear = () => ({ type: 'lookup clear' });
export const start = (name, key) => ({ type: 'lookup start', name, key }); export const lookupStart = (name, key) => ({ type: 'lookup start', name, key });
export const reverseLookupStart = (address) => ({ type: 'reverseLookup start', address });
export const success = (address) => ({ type: 'lookup success', result: address }); export const success = (action, result) => ({ type: `${action} success`, result: result });
export const fail = () => ({ type: 'lookup error' }); export const fail = (action) => ({ type: `${action} error` });
export const lookup = (name, key) => (dispatch, getState) => { export const lookup = (name, key) => (dispatch, getState) => {
const { contract } = getState(); const { contract } = getState();
if (!contract) { if (!contract) {
return; return;
} }
@ -35,16 +35,37 @@ export const lookup = (name, key) => (dispatch, getState) => {
.find((f) => f.name === 'getAddress'); .find((f) => f.name === 'getAddress');
name = name.toLowerCase(); name = name.toLowerCase();
dispatch(start(name, key)); dispatch(lookupStart(name, key));
getAddress.call({}, [sha3(name), key])
.then((address) => dispatch(success(address))) getAddress.call({}, [ sha3(name), key ])
.then((address) => dispatch(success('lookup', address)))
.catch((err) => { .catch((err) => {
console.error(`could not lookup ${key} for ${name}`); console.error(`could not lookup ${key} for ${name}`);
if (err) { if (err) {
console.error(err.stack); console.error(err.stack);
} }
dispatch(fail('lookup'));
dispatch(fail()); });
};
export const reverseLookup = (address) => (dispatch, getState) => {
const { contract } = getState();
if (!contract) {
return;
}
const reverse = contract.functions
.find((f) => f.name === 'reverse');
dispatch(reverseLookupStart(address));
reverse.call({}, [ address ])
.then((address) => dispatch(success('reverseLookup', address)))
.catch((err) => {
console.error(`could not lookup reverse for ${address}`);
if (err) {
console.error(err.stack);
}
dispatch(fail('reverseLookup'));
}); });
}; };

View File

@ -20,9 +20,7 @@
} }
.box { .box {
display: flex;
margin: 0 1em; margin: 0 1em;
} align-items: baseline;
.spacing {
margin-left: 1em;
} }

View File

@ -15,50 +15,65 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Card, CardHeader, CardText } from 'material-ui/Card'; import { Card, CardHeader, CardText } from 'material-ui/Card';
import TextField from 'material-ui/TextField'; import TextField from 'material-ui/TextField';
import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import SearchIcon from 'material-ui/svg-icons/action/search'; import SearchIcon from 'material-ui/svg-icons/action/search';
import { nullableProptype } from '~/util/proptypes'; import { nullableProptype } from '~/util/proptypes';
import renderAddress from '../ui/address.js'; import Address from '../ui/address.js';
import renderImage from '../ui/image.js'; import renderImage from '../ui/image.js';
import recordTypeSelect from '../ui/record-type-select.js'; import { clear, lookup, reverseLookup } from './actions';
import styles from './lookup.css'; import styles from './lookup.css';
export default class Lookup extends Component { class Lookup extends Component {
static propTypes = { static propTypes = {
actions: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
result: nullableProptype(PropTypes.string.isRequired), result: nullableProptype(PropTypes.string.isRequired),
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired clear: PropTypes.func.isRequired,
lookup: PropTypes.func.isRequired,
reverseLookup: PropTypes.func.isRequired
} }
state = { name: '', type: 'A' }; state = {
input: '', type: 'A'
};
render () { render () {
const name = this.state.name || this.props.name; const { input, type } = this.state;
const type = this.state.type || this.props.type; const { result } = this.props;
const { result, accounts, contacts } = this.props;
let output = ''; let output = '';
if (result) { if (result) {
if (type === 'A') { if (type === 'A') {
output = (<code>{ renderAddress(result, accounts, contacts, false) }</code>); output = (
<code>
<Address
address={ result }
shortenHash={ false }
/>
</code>
);
} else if (type === 'IMG') { } else if (type === 'IMG') {
output = renderImage(result); output = renderImage(result);
} else if (type === 'CONTENT') { } else if (type === 'CONTENT') {
output = (<div> output = (
<code>{ result }</code> <div>
<p>This is most likely just the hash of the content you are looking for</p> <code>{ result }</code>
</div>); <p>Keep in mind that this is most likely the hash of the content you are looking for.</p>
</div>
);
} else { } else {
output = (<code>{ result }</code>); output = (
<code>{ result }</code>
);
} }
} }
@ -67,14 +82,20 @@ export default class Lookup extends Component {
<CardHeader title={ 'Query the Registry' } /> <CardHeader title={ 'Query the Registry' } />
<div className={ styles.box }> <div className={ styles.box }>
<TextField <TextField
className={ styles.spacing } hintText={ type === 'reverse' ? 'address' : 'name' }
hintText='name' value={ input }
value={ name } onChange={ this.onInputChange }
onChange={ this.onNameChange }
/> />
{ recordTypeSelect(type, this.onTypeChange, styles.spacing) } <DropDownMenu
value={ type }
onChange={ this.onTypeChange }
>
<MenuItem value='A' primaryText='A Ethereum address' />
<MenuItem value='IMG' primaryText='IMG  hash of a picture in the blockchain' />
<MenuItem value='CONTENT' primaryText='CONTENT  hash of a data in the blockchain' />
<MenuItem value='reverse' primaryText='reverse find a name for an address' />
</DropDownMenu>
<RaisedButton <RaisedButton
className={ styles.spacing }
label='Lookup' label='Lookup'
primary primary
icon={ <SearchIcon /> } icon={ <SearchIcon /> }
@ -86,14 +107,30 @@ export default class Lookup extends Component {
); );
} }
onNameChange = (e) => { onInputChange = (e) => {
this.setState({ name: e.target.value }); this.setState({ input: e.target.value });
}; };
onTypeChange = (e, i, type) => { onTypeChange = (e, i, type) => {
this.setState({ type }); this.setState({ type });
this.props.actions.clear(); this.props.clear();
}; };
onLookupClick = () => { onLookupClick = () => {
this.props.actions.lookup(this.state.name, this.state.type); const { input, type } = this.state;
if (type === 'reverse') {
this.props.reverseLookup(input);
} else {
this.props.lookup(input, type);
}
}; };
} }
const mapStateToProps = (state) => state.lookup;
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
clear, lookup, reverseLookup
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Lookup);

View File

@ -14,39 +14,34 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isStage } from '../util/actions';
const initialState = { const initialState = {
pending: false, pending: false,
name: '', type: '',
result: null result: null
}; };
export default (state = initialState, action) => { export default (state = initialState, action) => {
if (action.type === 'lookup clear') { const { type } = action;
return { ...state, result: null };
if (type.slice(0, 7) !== 'lookup ' && type.slice(0, 14) !== 'reverseLookup ') {
return state;
} }
if (action.type === 'lookup start') { if (isStage('clear', action)) {
return { return { pending: state.pending, result: null };
pending: true,
name: action.name, type: action.entry,
result: null
};
} }
if (action.type === 'lookup error') { if (isStage('start', action)) {
return { return { pending: true, result: null };
pending: false,
name: initialState.name, type: initialState.type,
result: null
};
} }
if (action.type === 'lookup success') { if (isStage('error', action)) {
return { return { pending: false, result: null };
pending: false, }
name: initialState.name, type: initialState.type,
result: action.result if (isStage('success', action)) {
}; return { pending: false, result: action.result };
} }
return state; return state;

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { sha3, api } from '../parity.js'; import { sha3, api } from '../parity.js';
import postTx from '../util/post-tx';
const alreadyQueued = (queue, action, name) => const alreadyQueued = (queue, action, name) =>
!!queue.find((entry) => entry.action === action && entry.name === name); !!queue.find((entry) => entry.action === action && entry.name === name);
@ -30,42 +31,32 @@ export const reserve = (name) => (dispatch, getState) => {
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
const fee = state.fee; const fee = state.fee;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase();
if (alreadyQueued(state.names.queue, 'reserve', name)) { if (alreadyQueued(state.names.queue, 'reserve', name)) {
return; return;
} }
const reserve = contract.functions.find((f) => f.name === 'reserve'); const reserve = contract.functions.find((f) => f.name === 'reserve');
name = name.toLowerCase(); dispatch(reserveStart(name));
const options = { const options = {
from: account.address, from: account.address,
value: fee value: fee
}; };
const values = [ sha3(name) ]; const values = [
sha3(name)
];
dispatch(reserveStart(name)); postTx(api, reserve, options, values)
.then((txHash) => {
reserve.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return reserve.postTransaction(options, values);
})
.then((requestId) => {
return api.pollMethod('parity_checkRequest', requestId);
})
.then((txhash) => {
dispatch(reserveSuccess(name)); dispatch(reserveSuccess(name));
}) })
.catch((err) => { .catch((err) => {
if (err && err.type === 'REQUEST_REJECTED') {
return dispatch(reserveFail(name));
}
console.error(`could not reserve ${name}`); console.error(`could not reserve ${name}`);
if (err) { if (err) {
@ -86,38 +77,31 @@ export const drop = (name) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase();
if (alreadyQueued(state.names.queue, 'drop', name)) { if (alreadyQueued(state.names.queue, 'drop', name)) {
return; return;
} }
const drop = contract.functions.find((f) => f.name === 'drop'); const drop = contract.functions.find((f) => f.name === 'drop');
name = name.toLowerCase();
const options = { from: account.address };
const values = [ sha3(name) ];
dispatch(dropStart(name)); dispatch(dropStart(name));
drop.estimateGas(options, values)
.then((gas) => { const options = {
options.gas = gas.mul(1.2).toFixed(0); from: account.address
return drop.postTransaction(options, values); };
}) const values = [
.then((requestId) => { sha3(name)
return api.pollMethod('parity_checkRequest', requestId); ];
})
postTx(api, drop, options, values)
.then((txhash) => { .then((txhash) => {
dispatch(dropSuccess(name)); dispatch(dropSuccess(name));
}) })
.catch((err) => { .catch((err) => {
if (err && err.type === 'REQUEST_REJECTED') {
dispatch(reserveFail(name));
}
console.error(`could not drop ${name}`); console.error(`could not drop ${name}`);
if (err) { if (err) {

View File

@ -20,7 +20,8 @@
} }
.box { .box {
margin: 0 1em 1em 1em; display: flex;
align-items: baseline;
} }
.spacing { .spacing {

View File

@ -15,6 +15,8 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Card, CardHeader, CardText } from 'material-ui/Card'; import { Card, CardHeader, CardText } from 'material-ui/Card';
import TextField from 'material-ui/TextField'; import TextField from 'material-ui/TextField';
import DropDownMenu from 'material-ui/DropDownMenu'; import DropDownMenu from 'material-ui/DropDownMenu';
@ -24,6 +26,7 @@ import CheckIcon from 'material-ui/svg-icons/navigation/check';
import { fromWei } from '../parity.js'; import { fromWei } from '../parity.js';
import { reserve, drop } from './actions';
import styles from './names.css'; import styles from './names.css';
const useSignerText = (<p>Use the <a href='/#/signer' className={ styles.link } target='_blank'>Signer</a> to authenticate the following changes.</p>); const useSignerText = (<p>Use the <a href='/#/signer' className={ styles.link } target='_blank'>Signer</a> to authenticate the following changes.</p>);
@ -72,13 +75,15 @@ const renderQueue = (queue) => {
); );
}; };
export default class Names extends Component { class Names extends Component {
static propTypes = { static propTypes = {
actions: PropTypes.object.isRequired,
fee: PropTypes.object.isRequired, fee: PropTypes.object.isRequired,
pending: PropTypes.bool.isRequired, pending: PropTypes.bool.isRequired,
queue: PropTypes.array.isRequired queue: PropTypes.array.isRequired,
reserve: PropTypes.func.isRequired,
drop: PropTypes.func.isRequired
} }
state = { state = {
@ -117,27 +122,29 @@ export default class Names extends Component {
: (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>) : (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>)
) )
} }
<TextField <div className={ styles.box }>
hintText='name' <TextField
value={ name } hintText='name'
onChange={ this.onNameChange } value={ name }
/> onChange={ this.onNameChange }
<DropDownMenu />
disabled={ pending } <DropDownMenu
value={ action } disabled={ pending }
onChange={ this.onActionChange } value={ action }
> onChange={ this.onActionChange }
<MenuItem value='reserve' primaryText='reserve this name' /> >
<MenuItem value='drop' primaryText='drop this name' /> <MenuItem value='reserve' primaryText='reserve this name' />
</DropDownMenu> <MenuItem value='drop' primaryText='drop this name' />
<RaisedButton </DropDownMenu>
disabled={ pending } <RaisedButton
className={ styles.spacing } disabled={ pending }
label={ action === 'reserve' ? 'Reserve' : 'Drop' } className={ styles.spacing }
primary label={ action === 'reserve' ? 'Reserve' : 'Drop' }
icon={ <CheckIcon /> } primary
onTouchTap={ this.onSubmitClick } icon={ <CheckIcon /> }
/> onTouchTap={ this.onSubmitClick }
/>
</div>
{ queue.length > 0 { queue.length > 0
? (<div>{ useSignerText }{ renderQueue(queue) }</div>) ? (<div>{ useSignerText }{ renderQueue(queue) }</div>)
: null : null
@ -156,9 +163,14 @@ export default class Names extends Component {
onSubmitClick = () => { onSubmitClick = () => {
const { action, name } = this.state; const { action, name } = this.state;
if (action === 'reserve') { if (action === 'reserve') {
this.props.actions.reserve(name); this.props.reserve(name);
} else if (action === 'drop') { } else if (action === 'drop') {
this.props.actions.drop(name); this.props.drop(name);
} }
}; };
} }
const mapStateToProps = (state) => ({ ...state.names, fee: state.fee });
const mapDispatchToProps = (dispatch) => bindActionCreators({ reserve, drop }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Names);

View File

@ -14,36 +14,38 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions';
const initialState = { const initialState = {
pending: false, pending: false,
queue: [] queue: []
}; };
export default (state = initialState, action) => { export default (state = initialState, action) => {
if (action.type === 'names reserve start') { if (isAction('names', 'reserve', action)) {
return { ...state, pending: true }; if (isStage('start', action)) {
} return {
if (action.type === 'names reserve success') { ...state, pending: true,
return { queue: addToQueue(state.queue, 'reserve', action.name)
...state, pending: false, };
queue: state.queue.concat({ action: 'reserve', name: action.name }) } else if (isStage('success', action) || isStage('fail', action)) {
}; return {
} ...state, pending: false,
if (action.type === 'names reserve fail') { queue: removeFromQueue(state.queue, 'reserve', action.name)
return { ...state, pending: false }; };
} }
} else if (isAction('names', 'drop', action)) {
if (action.type === 'names drop start') { if (isStage('start', action)) {
return { ...state, pending: true }; return {
} ...state, pending: true,
if (action.type === 'names drop success') { queue: addToQueue(state.queue, 'drop', action.name)
return { };
...state, pending: false, } else if (isStage('success', action) || isStage('fail', action)) {
queue: state.queue.concat({ action: 'drop', name: action.name }) return {
}; ...state, pending: false,
} queue: removeFromQueue(state.queue, 'drop', action.name)
if (action.type === 'names drop fail') { };
return { ...state, pending: false }; }
} }
return state; return state;

View File

@ -1,4 +1,21 @@
import { sha3 } from '../parity.js'; // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { sha3, api } from '../parity.js';
import postTx from '../util/post-tx';
export const start = (name, key, value) => ({ type: 'records update start', name, key, value }); export const start = (name, key, value) => ({ type: 'records update start', name, key, value });
@ -10,25 +27,28 @@ export const update = (name, key, value) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
const fnName = key === 'A' ? 'setAddress' : 'set';
const fn = contract.functions.find((f) => f.name === fnName);
name = name.toLowerCase(); name = name.toLowerCase();
const options = { from: account.address };
const values = [ sha3(name), key, value ]; const fnName = key === 'A' ? 'setAddress' : 'set';
const setAddress = contract.functions.find((f) => f.name === fnName);
dispatch(start(name, key, value)); dispatch(start(name, key, value));
fn.estimateGas(options, values)
.then((gas) => { const options = {
options.gas = gas.mul(1.2).toFixed(0); from: account.address
return fn.postTransaction(options, values); };
}) const values = [
.then((data) => { sha3(name),
key,
value
];
postTx(api, setAddress, options, values)
.then((txHash) => {
dispatch(success()); dispatch(success());
}).catch((err) => { }).catch((err) => {
console.error(`could not update ${key} record of ${name}`); console.error(`could not update ${key} record of ${name}`);

View File

@ -23,6 +23,16 @@
margin-top: 0; margin-top: 0;
} }
.box {
display: flex;
align-items: baseline;
}
.spacing { .spacing {
margin-left: 1em; margin-left: 1em;
} }
.button {
flex-grow: 0;
flex-shrink: 0;
}

View File

@ -15,22 +15,27 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Card, CardHeader, CardText } from 'material-ui/Card'; import { Card, CardHeader, CardText } from 'material-ui/Card';
import TextField from 'material-ui/TextField'; import TextField from 'material-ui/TextField';
import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import SaveIcon from 'material-ui/svg-icons/content/save'; import SaveIcon from 'material-ui/svg-icons/content/save';
import recordTypeSelect from '../ui/record-type-select.js'; import { update } from './actions';
import styles from './records.css'; import styles from './records.css';
export default class Records extends Component { class Records extends Component {
static propTypes = { static propTypes = {
actions: PropTypes.object.isRequired,
pending: PropTypes.bool.isRequired, pending: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
value: PropTypes.string.isRequired value: PropTypes.string.isRequired,
update: PropTypes.func.isRequired
} }
state = { name: '', type: 'A', value: '' }; state = { name: '', type: 'A', value: '' };
@ -48,28 +53,36 @@ export default class Records extends Component {
<p className={ styles.noSpacing }> <p className={ styles.noSpacing }>
You can only modify entries of names that you previously registered. You can only modify entries of names that you previously registered.
</p> </p>
<div className={ styles.box }>
<TextField <TextField
className={ styles.spacing } hintText='name'
hintText='name' value={ name }
value={ name } onChange={ this.onNameChange }
onChange={ this.onNameChange } />
/> <DropDownMenu
{ recordTypeSelect(type, this.onTypeChange, styles.spacing) } value={ type }
<TextField onChange={ this.onTypeChange }
className={ styles.spacing } >
hintText='value' <MenuItem value='A' primaryText='A Ethereum address' />
value={ value } <MenuItem value='IMG' primaryText='IMG  hash of a picture in the blockchain' />
onChange={ this.onValueChange } <MenuItem value='CONTENT' primaryText='CONTENT  hash of a data in the blockchain' />
/> </DropDownMenu>
<RaisedButton <TextField
disabled={ pending } hintText='value'
className={ styles.spacing } value={ value }
label='Save' onChange={ this.onValueChange }
primary />
icon={ <SaveIcon /> } <div className={ styles.button }>
onTouchTap={ this.onSaveClick } <RaisedButton
/> disabled={ pending }
className={ styles.spacing }
label='Save'
primary
icon={ <SaveIcon /> }
onTouchTap={ this.onSaveClick }
/>
</div>
</div>
</CardText> </CardText>
</Card> </Card>
); );
@ -86,6 +99,11 @@ export default class Records extends Component {
}; };
onSaveClick = () => { onSaveClick = () => {
const { name, type, value } = this.state; const { name, type, value } = this.state;
this.props.actions.update(name, type, value); this.props.update(name, type, value);
}; };
} }
const mapStateToProps = (state) => state.records;
const mapDispatchToProps = (dispatch) => bindActionCreators({ update }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Records);

View File

@ -1,21 +1,39 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isAction, isStage } from '../util/actions';
const initialState = { const initialState = {
pending: false, pending: false,
name: '', type: '', value: '' name: '', type: '', value: ''
}; };
export default (state = initialState, action) => { export default (state = initialState, action) => {
if (action.type === 'records update start') { if (!isAction('records', 'update', action)) {
return { return state;
...state,
pending: true,
name: action.name, type: action.entry, value: action.value
};
} }
if (action.type === 'records update error' || action.type === 'records update success') { if (isStage('start', action)) {
return { return {
...state, ...state, pending: true,
pending: false, name: action.name, type: action.entry, value: action.value
};
} else if (isStage('success', action) || isStage('fail', action)) {
return {
...state, pending: false,
name: initialState.name, type: initialState.type, value: initialState.value name: initialState.name, type: initialState.type, value: initialState.value
}; };
} }

View File

@ -0,0 +1,92 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { api } from '../parity.js';
import postTx from '../util/post-tx';
export const start = (action, name, address) => ({ type: `reverse ${action} start`, name, address });
export const success = (action) => ({ type: `reverse ${action} success` });
export const fail = (action) => ({ type: `reverse ${action} error` });
export const propose = (name, address) => (dispatch, getState) => {
const state = getState();
const account = state.accounts.selected;
const contract = state.contract;
if (!contract || !account) {
return;
}
name = name.toLowerCase();
const proposeReverse = contract.functions.find((f) => f.name === 'proposeReverse');
dispatch(start('propose', name, address));
const options = {
from: account.address
};
const values = [
name,
address
];
postTx(api, proposeReverse, options, values)
.then((txHash) => {
dispatch(success('propose'));
})
.catch((err) => {
console.error(`could not propose reverse ${name} for address ${address}`);
if (err) {
console.error(err.stack);
}
dispatch(fail('propose'));
});
};
export const confirm = (name) => (dispatch, getState) => {
const state = getState();
const account = state.accounts.selected;
const contract = state.contract;
if (!contract || !account) {
return;
}
name = name.toLowerCase();
const confirmReverse = contract.functions.find((f) => f.name === 'confirmReverse');
dispatch(start('confirm', name));
const options = {
from: account.address
};
const values = [
name
];
postTx(api, confirmReverse, options, values)
.then((txHash) => {
dispatch(success('confirm'));
})
.catch((err) => {
console.error(`could not confirm reverse ${name}`);
if (err) {
console.error(err.stack);
}
dispatch(fail('confirm'));
});
};

View File

@ -0,0 +1 @@
export default from './reverse';

View File

@ -0,0 +1,68 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isAction, isStage } from '../util/actions';
const initialState = {
pending: false,
queue: []
};
export default (state = initialState, action) => {
if (isAction('reverse', 'propose', action)) {
if (isStage('start', action)) {
return {
...state, pending: true,
queue: state.queue.concat({
action: 'propose',
name: action.name,
address: action.address
})
};
} else if (isStage('success', action) || isStage('fail', action)) {
return {
...state, pending: false,
queue: state.queue.filter((e) =>
e.action === 'propose' &&
e.name === action.name &&
e.address === action.address
)
};
}
}
if (isAction('reverse', 'confirm', action)) {
if (isStage('start', action)) {
return {
...state, pending: true,
queue: state.queue.concat({
action: 'confirm',
name: action.name
})
};
} else if (isStage('success', action) || isStage('fail', action)) {
return {
...state, pending: false,
queue: state.queue.filter((e) =>
e.action === 'confirm' &&
e.name === action.name
)
};
}
}
return state;
};

View File

@ -0,0 +1,39 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.reverse {
margin: 1em;
}
.noSpacing {
margin-top: 0;
}
.box {
display: flex;
align-items: baseline;
}
.spacing {
margin-right: 1em;
}
.button {
flex-grow: 0;
flex-shrink: 0;
}

View File

@ -0,0 +1,136 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
Card, CardHeader, CardText, TextField, DropDownMenu, MenuItem, RaisedButton, AddIcon, CheckIcon
} from 'material-ui';
import { propose, confirm } from './actions';
import styles from './reverse.css';
class Reverse extends Component {
static propTypes = {
pending: PropTypes.bool.isRequired,
queue: PropTypes.array.isRequired,
propose: PropTypes.func.isRequired,
confirm: PropTypes.func.isRequired
}
state = {
action: 'propose',
name: '',
address: ''
};
render () {
const { pending } = this.props;
const { action, address, name } = this.state;
const explanation = action === 'propose'
? (
<p className={ styles.noSpacing }>
To propose a reverse entry for <code>foo</code>, you have to be the owner of it.
</p>
) : (
<p className={ styles.noSpacing }>
To confirm a proposal, send the transaction from the account that the name has been proposed for.
</p>
);
let addressInput = null;
if (action === 'propose') {
addressInput = (
<TextField
className={ styles.spacing }
hintText='address'
value={ address }
onChange={ this.onAddressChange }
/>
);
}
return (
<Card className={ styles.reverse }>
<CardHeader title={ 'Manage Reverse Names' } />
<CardText>
<p className={ styles.noSpacing }>
<strong>
To make others to find the name of an address using the registry, you can propose & confirm reverse entries.
</strong>
</p>
{ explanation }
<div className={ styles.box }>
<DropDownMenu
disabled={ pending }
value={ action }
onChange={ this.onActionChange }
>
<MenuItem value='propose' primaryText='propose a reverse entry' />
<MenuItem value='confirm' primaryText='confirm a reverse entry' />
</DropDownMenu>
{ addressInput }
<TextField
className={ styles.spacing }
hintText='name'
value={ name }
onChange={ this.onNameChange }
/>
<div className={ styles.button }>
<RaisedButton
disabled={ pending }
label={ action === 'propose' ? 'Propose' : 'Confirm' }
primary
icon={ action === 'propose' ? <AddIcon /> : <CheckIcon /> }
onTouchTap={ this.onSubmitClick }
/>
</div>
</div>
</CardText>
</Card>
);
}
onNameChange = (e) => {
this.setState({ name: e.target.value });
};
onAddressChange = (e) => {
this.setState({ address: e.target.value });
};
onActionChange = (e, i, action) => {
this.setState({ action });
};
onSubmitClick = () => {
const { action, name, address } = this.state;
if (action === 'propose') {
this.props.propose(name, address);
} else if (action === 'confirm') {
this.props.confirm(name);
}
};
}
const mapStateToProps = (state) => state.reverse;
const mapDispatchToProps = (dispatch) => bindActionCreators({ propose, confirm }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Reverse);

View File

@ -23,63 +23,76 @@ import * as lookup from './Lookup/actions.js';
import * as events from './Events/actions.js'; import * as events from './Events/actions.js';
import * as names from './Names/actions.js'; import * as names from './Names/actions.js';
import * as records from './Records/actions.js'; import * as records from './Records/actions.js';
import * as reverse from './Reverse/actions.js';
export { addresses, accounts, lookup, events, names, records }; export { addresses, accounts, lookup, events, names, records, reverse };
export const setIsTestnet = (isTestnet) => ({ type: 'set isTestnet', isTestnet });
export const fetchIsTestnet = () => (dispatch) =>
api.net.version()
.then((netVersion) => {
dispatch(setIsTestnet(
netVersion === '2' || // morden
netVersion === '3' // ropsten
));
})
.catch((err) => {
console.error('could not check if testnet');
if (err) {
console.error(err.stack);
}
});
export const setContract = (contract) => ({ type: 'set contract', contract }); export const setContract = (contract) => ({ type: 'set contract', contract });
export const fetchContract = () => (dispatch) => export const fetchContract = () => (dispatch) =>
api.parity.registryAddress() api.parity.registryAddress()
.then((address) => { .then((address) => {
const contract = api.newContract(registryAbi, address); const contract = api.newContract(registryAbi, address);
dispatch(setContract(contract)); dispatch(setContract(contract));
dispatch(fetchFee()); dispatch(fetchFee());
dispatch(fetchOwner()); dispatch(fetchOwner());
}) })
.catch((err) => { .catch((err) => {
console.error('could not fetch contract'); console.error('could not fetch contract');
if (err) {
if (err) { console.error(err.stack);
console.error(err.stack); }
} });
});
export const setFee = (fee) => ({ type: 'set fee', fee }); export const setFee = (fee) => ({ type: 'set fee', fee });
const fetchFee = () => (dispatch, getState) => { const fetchFee = () => (dispatch, getState) => {
const { contract } = getState(); const { contract } = getState();
if (!contract) { if (!contract) {
return; return;
} }
contract.instance.fee.call() contract.instance.fee.call()
.then((fee) => dispatch(setFee(fee))) .then((fee) => dispatch(setFee(fee)))
.catch((err) => { .catch((err) => {
console.error('could not fetch fee'); console.error('could not fetch fee');
if (err) {
if (err) { console.error(err.stack);
console.error(err.stack); }
} });
});
}; };
export const setOwner = (owner) => ({ type: 'set owner', owner }); export const setOwner = (owner) => ({ type: 'set owner', owner });
export const fetchOwner = () => (dispatch, getState) => { export const fetchOwner = () => (dispatch, getState) => {
const { contract } = getState(); const { contract } = getState();
if (!contract) { if (!contract) {
return; return;
} }
contract.instance.owner.call() contract.instance.owner.call()
.then((owner) => dispatch(setOwner(owner))) .then((owner) => dispatch(setOwner(owner)))
.catch((err) => { .catch((err) => {
console.error('could not fetch owner'); console.error('could not fetch owner');
if (err) {
if (err) { console.error(err.stack);
console.error(err.stack); }
} });
});
}; };

View File

@ -20,6 +20,10 @@ import lookupReducer from './Lookup/reducers.js';
import eventsReducer from './Events/reducers.js'; import eventsReducer from './Events/reducers.js';
import namesReducer from './Names/reducers.js'; import namesReducer from './Names/reducers.js';
import recordsReducer from './Records/reducers.js'; import recordsReducer from './Records/reducers.js';
import reverseReducer from './Reverse/reducers.js';
const isTestnetReducer = (state = null, action) =>
action.type === 'set isTestnet' ? action.isTestnet : state;
const contractReducer = (state = null, action) => const contractReducer = (state = null, action) =>
action.type === 'set contract' ? action.contract : state; action.type === 'set contract' ? action.contract : state;
@ -31,6 +35,7 @@ const ownerReducer = (state = null, action) =>
action.type === 'set owner' ? action.owner : state; action.type === 'set owner' ? action.owner : state;
const initialState = { const initialState = {
isTestnet: isTestnetReducer(undefined, { type: '' }),
accounts: accountsReducer(undefined, { type: '' }), accounts: accountsReducer(undefined, { type: '' }),
contacts: contactsReducer(undefined, { type: '' }), contacts: contactsReducer(undefined, { type: '' }),
contract: contractReducer(undefined, { type: '' }), contract: contractReducer(undefined, { type: '' }),
@ -39,10 +44,12 @@ const initialState = {
lookup: lookupReducer(undefined, { type: '' }), lookup: lookupReducer(undefined, { type: '' }),
events: eventsReducer(undefined, { type: '' }), events: eventsReducer(undefined, { type: '' }),
names: namesReducer(undefined, { type: '' }), names: namesReducer(undefined, { type: '' }),
records: recordsReducer(undefined, { type: '' }) records: recordsReducer(undefined, { type: '' }),
reverse: reverseReducer(undefined, { type: '' })
}; };
export default (state = initialState, action) => ({ export default (state = initialState, action) => ({
isTestnet: isTestnetReducer(state.isTestnet, action),
accounts: accountsReducer(state.accounts, action), accounts: accountsReducer(state.accounts, action),
contacts: contactsReducer(state.contacts, action), contacts: contactsReducer(state.contacts, action),
contract: contractReducer(state.contract, action), contract: contractReducer(state.contract, action),
@ -51,5 +58,6 @@ export default (state = initialState, action) => ({
lookup: lookupReducer(state.lookup, action), lookup: lookupReducer(state.lookup, action),
events: eventsReducer(state.events, action), events: eventsReducer(state.events, action),
names: namesReducer(state.names, action), names: namesReducer(state.names, action),
records: recordsReducer(state.records, action) records: recordsReducer(state.records, action),
reverse: reverseReducer(state.reverse, action)
}); });

View File

@ -0,0 +1,41 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.container {
display: inline-block;
vertical-align: middle;
line-height: 24px;
}
.align {
display: inline-block;
vertical-align: top;
line-height: 24px;
}
.link {
text-decoration: none;
color: inherit;
&:hover {
text-decoration: underline;
}
& abbr {
text-decoration: inherit;
}
}

View File

@ -14,34 +14,85 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react'; import React, { Component, PropTypes } from 'react';
import renderHash from './hash'; import { connect } from 'react-redux';
import Hash from './hash';
import etherscanUrl from '../util/etherscan-url';
import IdentityIcon from '../IdentityIcon'; import IdentityIcon from '../IdentityIcon';
const container = { import styles from './address.css';
display: 'inline-block',
verticalAlign: 'middle',
height: '24px'
};
const align = {
display: 'inline-block',
verticalAlign: 'top',
lineHeight: '24px'
};
export default (address, accounts, contacts, shortenHash = true) => { class Address extends Component {
let caption; static propTypes = {
if (accounts[address]) { address: PropTypes.string.isRequired,
caption = (<abbr title={ address } style={ align }>{ accounts[address].name || address }</abbr>); accounts: PropTypes.object.isRequired,
} else if (contacts[address]) { contacts: PropTypes.object.isRequired,
caption = (<abbr title={ address } style={ align }>{ contacts[address].name || address }</abbr>); isTestnet: PropTypes.bool.isRequired,
} else { key: PropTypes.string,
caption = (<code style={ align }>{ shortenHash ? renderHash(address) : address }</code>); shortenHash: PropTypes.bool
} }
return (
<div style={ container }> static defaultProps = {
<IdentityIcon address={ address } style={ align } /> key: 'address',
{ caption } shortenHash: true
</div> }
);
}; render () {
const { address, accounts, contacts, isTestnet, key, shortenHash } = this.props;
let caption;
if (accounts[address] || contacts[address]) {
const name = (accounts[address] || contacts[address] || {}).name;
caption = (
<a
className={ styles.link }
href={ etherscanUrl(address, isTestnet) }
target='_blank'
>
<abbr
title={ address }
className={ styles.align }
>
{ name || address }
</abbr>
</a>
);
} else {
caption = (
<code className={ styles.align }>
{ shortenHash ? (
<Hash
hash={ address }
linked
/>
) : address }
</code>
);
}
return (
<div
key={ key }
className={ styles.container }
>
<IdentityIcon
address={ address }
className={ styles.align }
/>
{ caption }
</div>
);
}
}
export default connect(
// mapStateToProps
(state) => ({
accounts: state.accounts.all,
contacts: state.contacts,
isTestnet: state.isTestnet
}),
// mapDispatchToProps
null
)(Address);

View File

@ -0,0 +1,25 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.link {
text-decoration: none;
color: inherit;
&:hover {
text-decoration: underline;
}
}

View File

@ -14,11 +14,53 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
export default (hash) => { import etherscanUrl from '../util/etherscan-url';
const shortened = hash.length > (2 + 9 + 9)
? hash.substr(2, 9) + '...' + hash.slice(-9) import styles from './hash.css';
: hash.slice(2);
return (<abbr title={ hash }>{ shortened }</abbr>); const leading0x = /^0x/;
};
class Hash extends Component {
static propTypes = {
hash: PropTypes.string.isRequired,
isTestnet: PropTypes.bool.isRequired,
linked: PropTypes.bool
}
static defaultProps = {
linked: false
}
render () {
const { hash, isTestnet, linked } = this.props;
let shortened = hash.toLowerCase().replace(leading0x, '');
shortened = shortened.length > (6 + 6)
? shortened.substr(0, 6) + '...' + shortened.slice(-6)
: shortened;
if (linked) {
return (
<a
className={ styles.link }
href={ etherscanUrl(hash, isTestnet) }
target='_blank'
>
<abbr title={ hash }>{ shortened }</abbr>
</a>
);
}
return (<abbr title={ hash }>{ shortened }</abbr>);
}
}
export default connect(
(state) => ({ // mapStateToProps
isTestnet: state.isTestnet
}),
null // mapDispatchToProps
)(Hash);

View File

@ -14,14 +14,18 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react'; export const isAction = (ns, type, action) => {
import DropDownMenu from 'material-ui/DropDownMenu'; return action.type.slice(0, ns.length + 1 + type.length) === `${ns} ${type}`;
import MenuItem from 'material-ui/MenuItem'; };
export default (value, onSelect, className = '') => ( export const isStage = (stage, action) => {
<DropDownMenu className={ className } value={ value } onChange={ onSelect }> return action.type.slice(-1 - stage.length) === ` ${stage}`;
<MenuItem value='A' primaryText='A Ethereum address' /> };
<MenuItem value='IMG' primaryText='IMG  hash of a picture in the blockchain' />
<MenuItem value='CONTENT' primaryText='CONTENT  hash of a data in the blockchain' /> export const addToQueue = (queue, action, name) => {
</DropDownMenu> return queue.concat({ action, name });
); };
export const removeFromQueue = (queue, action, name) => {
return queue.filter((e) => e.action === action && e.name === name);
};

View File

@ -0,0 +1,26 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
const leading0x = /^0x/;
const etherscanUrl = (hash, isTestnet) => {
hash = hash.toLowerCase().replace(leading0x, '');
const type = hash.length === 40 ? 'address' : 'tx';
return `https://${isTestnet ? 'testnet.' : ''}etherscan.io/${type}/0x${hash}`;
};
export default etherscanUrl;

View File

@ -0,0 +1,36 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
const postTx = (api, method, opt = {}, values = []) => {
opt = Object.assign({}, opt);
return method.estimateGas(opt, values)
.then((gas) => {
opt.gas = gas.mul(1.2).toFixed(0);
return method.postTransaction(opt, values);
})
.then((reqId) => {
return api.pollMethod('parity_checkRequest', reqId);
})
.catch((err) => {
if (err && err.type === 'REQUEST_REJECTED') {
throw new Error('The request has been rejected.');
}
throw err;
});
};
export default postTx;