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/>.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import IconMenu from 'material-ui/IconMenu';
import IconButton from 'material-ui/IconButton/IconButton';
import AccountIcon from 'material-ui/svg-icons/action/account-circle';
import MenuItem from 'material-ui/MenuItem';
import IdentityIcon from '../IdentityIcon';
import renderAddress from '../ui/address';
import Address from '../ui/address';
import { select } from './actions';
import styles from './accounts.css';
export default class Accounts extends Component {
class Accounts extends Component {
static propTypes = {
actions: PropTypes.object.isRequired,
all: PropTypes.object.isRequired,
selected: PropTypes.object
selected: PropTypes.object,
select: PropTypes.func.isRequired
}
render () {
@ -41,8 +45,17 @@ export default class Accounts extends Component {
const accountsButton = (
<IconButton className={ styles.button }>
{ 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>);
@ -61,20 +74,27 @@ export default class Accounts extends Component {
}
renderAccount = (account) => {
const { all, selected } = this.props;
const { selected } = this.props;
const isSelected = selected && selected.address === account.address;
return (
<MenuItem
key={ account.address } value={ account.address }
checked={ isSelected } insetChildren={ !isSelected }
key={ account.address }
value={ account.address }
checked={ isSelected }
insetChildren={ !isSelected }
>
{ renderAddress(account.address, all, {}) }
<Address address={ account.address } />
</MenuItem>
);
};
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 Names from '../Names';
import Records from '../Records';
import Reverse from '../Reverse';
export default class Application extends Component {
static childContextTypes = {
@ -42,26 +43,14 @@ export default class Application extends Component {
}
static propTypes = {
actions: PropTypes.object.isRequired,
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired,
contract: 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
contract: nullableProptype(PropTypes.object).isRequired,
fee: nullableProptype(PropTypes.object).isRequired
};
render () {
const { api } = window.parity;
const {
actions,
accounts, contacts,
contract, fee,
lookup,
events
} = this.props;
const { contract, fee } = this.props;
let warning = null;
return (
@ -69,13 +58,13 @@ export default class Application extends Component {
{ warning }
<div className={ styles.header }>
<h1>RΞgistry</h1>
<Accounts { ...accounts } actions={ actions.accounts } />
<Accounts />
</div>
{ contract && fee ? (
<div>
<Lookup { ...lookup } accounts={ accounts.all } contacts={ contacts } actions={ actions.lookup } />
<Lookup />
{ this.renderActions() }
<Events { ...events } accounts={ accounts.all } contacts={ contacts } actions={ actions.events } />
<Events />
<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.
</div>
@ -88,15 +77,7 @@ export default class Application extends Component {
}
renderActions () {
const {
actions,
accounts,
fee,
names,
records
} = this.props;
const hasAccount = !!accounts.selected;
const hasAccount = !!this.props.accounts.selected;
if (!hasAccount) {
return (
@ -111,8 +92,9 @@ export default class Application extends Component {
return (
<div>
<Names { ...names } fee={ fee } actions={ actions.names } />
<Records { ...records } actions={ actions.records } />
<Names />
<Records />
<Reverse />
</div>
);
}

View File

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

View File

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

View File

@ -49,3 +49,9 @@
.eventsList td {
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/>.
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 Toggle from 'material-ui/Toggle';
import moment from 'moment';
import { bytesToHex } from '../parity';
import renderHash from '../ui/hash';
import renderAddress from '../ui/address';
import Hash from '../ui/hash';
import Address from '../ui/address';
import { subscribe, unsubscribe } from './actions';
import styles from './events.css';
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'
? classNames + ' ' + styles.pending : classNames;
return (
<tr key={ e.key } className={ classes }>
<td>{ renderAddress(e.parameters.owner.value, accounts, contacts) }</td>
<td><abbr title={ e.transaction }>{ verb }</abbr></td>
<td><code>{ renderHash(bytesToHex(e.parameters.name.value)) }</code></td>
<td>{ renderStatus(e.timestamp, e.state === 'pending') }</td>
<td>
<Address address={ e.parameters.owner.value } />
</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>
);
};
const renderDataChanged = (e, accounts, contacts) => {
const renderDataChanged = (e) => {
let classNames = styles.dataChanged;
if (e.state === 'pending') {
classNames += ' ' + styles.pending;
@ -64,12 +78,61 @@ const renderDataChanged = (e, accounts, contacts) => {
return (
<tr key={ e.key } className={ classNames }>
<td>{ renderAddress(e.parameters.owner.value, accounts, contacts) }</td>
<td><abbr title={ e.transaction }>updated</abbr></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>{ renderStatus(e.timestamp, e.state === 'pending') }</td>
</tr>
);
};
@ -77,22 +140,25 @@ const renderDataChanged = (e, accounts, contacts) => {
const eventTypes = {
Reserved: renderEvent(styles.reserved, 'reserved'),
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 = {
actions: PropTypes.object.isRequired,
subscriptions: PropTypes.object.isRequired,
pending: PropTypes.object.isRequired,
events: PropTypes.array.isRequired,
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired
pending: PropTypes.object.isRequired,
subscriptions: PropTypes.object.isRequired,
subscribe: PropTypes.func.isRequired,
unsubscribe: PropTypes.func.isRequired
}
render () {
const { subscriptions, pending, accounts, contacts } = this.props;
const { subscriptions, pending } = this.props;
const eventsObject = this.props.events
.filter((e) => eventTypes[e.type])
@ -122,7 +188,16 @@ export default class Events extends Component {
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 (
<Card className={ styles.events }>
@ -149,6 +224,13 @@ export default class Events extends Component {
onToggle={ this.onDataChangedToggle }
style={ inlineButton }
/>
<Toggle
label='Reverse Lookup'
toggled={ reverseToggled }
disabled={ reverseDisabled }
onToggle={ this.onReverseToggle }
style={ inlineButton }
/>
</CardActions>
<CardText>
<table className={ styles.eventsList }>
@ -162,33 +244,59 @@ export default class Events extends Component {
}
onReservedToggle = (e, isToggled) => {
const { pending, subscriptions, actions } = this.props;
const { pending, subscriptions, subscribe, unsubscribe } = this.props;
if (!pending.Reserved) {
if (isToggled && subscriptions.Reserved === null) {
actions.subscribe('Reserved');
subscribe('Reserved');
} else if (!isToggled && subscriptions.Reserved !== null) {
actions.unsubscribe('Reserved');
unsubscribe('Reserved');
}
}
};
onDroppedToggle = (e, isToggled) => {
const { pending, subscriptions, actions } = this.props;
const { pending, subscriptions, subscribe, unsubscribe } = this.props;
if (!pending.Dropped) {
if (isToggled && subscriptions.Dropped === null) {
actions.subscribe('Dropped');
subscribe('Dropped');
} else if (!isToggled && subscriptions.Dropped !== null) {
actions.unsubscribe('Dropped');
unsubscribe('Dropped');
}
}
};
onDataChangedToggle = (e, isToggled) => {
const { pending, subscriptions, actions } = this.props;
const { pending, subscriptions, subscribe, unsubscribe } = this.props;
if (!pending.DataChanged) {
if (isToggled && subscriptions.DataChanged === null) {
actions.subscribe('DataChanged');
subscribe('DataChanged');
} 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: {
Reserved: null,
Dropped: null,
DataChanged: null
DataChanged: null,
ReverseProposed: null,
ReverseConfirmed: null,
ReverseRemoved: null
},
pending: {
Reserved: false,
Dropped: false,
DataChanged: false
DataChanged: false,
ReverseProposed: false,
ReverseConfirmed: false,
ReverseRemoved: false
},
events: []
};

View File

@ -18,15 +18,15 @@ import { sha3 } from '../parity.js';
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) => {
const { contract } = getState();
if (!contract) {
return;
}
@ -35,16 +35,37 @@ export const lookup = (name, key) => (dispatch, getState) => {
.find((f) => f.name === 'getAddress');
name = name.toLowerCase();
dispatch(start(name, key));
getAddress.call({}, [sha3(name), key])
.then((address) => dispatch(success(address)))
dispatch(lookupStart(name, key));
getAddress.call({}, [ sha3(name), key ])
.then((address) => dispatch(success('lookup', address)))
.catch((err) => {
console.error(`could not lookup ${key} for ${name}`);
if (err) {
console.error(err.stack);
}
dispatch(fail());
dispatch(fail('lookup'));
});
};
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 {
display: flex;
margin: 0 1em;
}
.spacing {
margin-left: 1em;
align-items: baseline;
}

View File

@ -15,50 +15,65 @@
// 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 } from 'material-ui/Card';
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 SearchIcon from 'material-ui/svg-icons/action/search';
import { nullableProptype } from '~/util/proptypes';
import renderAddress from '../ui/address.js';
import Address from '../ui/address.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';
export default class Lookup extends Component {
class Lookup extends Component {
static propTypes = {
actions: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
type: 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 () {
const name = this.state.name || this.props.name;
const type = this.state.type || this.props.type;
const { result, accounts, contacts } = this.props;
const { input, type } = this.state;
const { result } = this.props;
let output = '';
if (result) {
if (type === 'A') {
output = (<code>{ renderAddress(result, accounts, contacts, false) }</code>);
output = (
<code>
<Address
address={ result }
shortenHash={ false }
/>
</code>
);
} else if (type === 'IMG') {
output = renderImage(result);
} else if (type === 'CONTENT') {
output = (<div>
<code>{ result }</code>
<p>This is most likely just the hash of the content you are looking for</p>
</div>);
output = (
<div>
<code>{ result }</code>
<p>Keep in mind that this is most likely the hash of the content you are looking for.</p>
</div>
);
} else {
output = (<code>{ result }</code>);
output = (
<code>{ result }</code>
);
}
}
@ -67,14 +82,20 @@ export default class Lookup extends Component {
<CardHeader title={ 'Query the Registry' } />
<div className={ styles.box }>
<TextField
className={ styles.spacing }
hintText='name'
value={ name }
onChange={ this.onNameChange }
hintText={ type === 'reverse' ? 'address' : 'name' }
value={ input }
onChange={ this.onInputChange }
/>
{ 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
className={ styles.spacing }
label='Lookup'
primary
icon={ <SearchIcon /> }
@ -86,14 +107,30 @@ export default class Lookup extends Component {
);
}
onNameChange = (e) => {
this.setState({ name: e.target.value });
onInputChange = (e) => {
this.setState({ input: e.target.value });
};
onTypeChange = (e, i, type) => {
this.setState({ type });
this.props.actions.clear();
this.props.clear();
};
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
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isStage } from '../util/actions';
const initialState = {
pending: false,
name: '', type: '',
result: null
};
export default (state = initialState, action) => {
if (action.type === 'lookup clear') {
return { ...state, result: null };
const { type } = action;
if (type.slice(0, 7) !== 'lookup ' && type.slice(0, 14) !== 'reverseLookup ') {
return state;
}
if (action.type === 'lookup start') {
return {
pending: true,
name: action.name, type: action.entry,
result: null
};
if (isStage('clear', action)) {
return { pending: state.pending, result: null };
}
if (action.type === 'lookup error') {
return {
pending: false,
name: initialState.name, type: initialState.type,
result: null
};
if (isStage('start', action)) {
return { pending: true, result: null };
}
if (action.type === 'lookup success') {
return {
pending: false,
name: initialState.name, type: initialState.type,
result: action.result
};
if (isStage('error', action)) {
return { pending: false, result: null };
}
if (isStage('success', action)) {
return { pending: false, result: action.result };
}
return state;

View File

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

View File

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

View File

@ -15,6 +15,8 @@
// 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 } from 'material-ui/Card';
import TextField from 'material-ui/TextField';
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 { reserve, drop } from './actions';
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>);
@ -72,13 +75,15 @@ const renderQueue = (queue) => {
);
};
export default class Names extends Component {
class Names extends Component {
static propTypes = {
actions: PropTypes.object.isRequired,
fee: PropTypes.object.isRequired,
pending: PropTypes.bool.isRequired,
queue: PropTypes.array.isRequired
queue: PropTypes.array.isRequired,
reserve: PropTypes.func.isRequired,
drop: PropTypes.func.isRequired
}
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>)
)
}
<TextField
hintText='name'
value={ name }
onChange={ this.onNameChange }
/>
<DropDownMenu
disabled={ pending }
value={ action }
onChange={ this.onActionChange }
>
<MenuItem value='reserve' primaryText='reserve this name' />
<MenuItem value='drop' primaryText='drop this name' />
</DropDownMenu>
<RaisedButton
disabled={ pending }
className={ styles.spacing }
label={ action === 'reserve' ? 'Reserve' : 'Drop' }
primary
icon={ <CheckIcon /> }
onTouchTap={ this.onSubmitClick }
/>
<div className={ styles.box }>
<TextField
hintText='name'
value={ name }
onChange={ this.onNameChange }
/>
<DropDownMenu
disabled={ pending }
value={ action }
onChange={ this.onActionChange }
>
<MenuItem value='reserve' primaryText='reserve this name' />
<MenuItem value='drop' primaryText='drop this name' />
</DropDownMenu>
<RaisedButton
disabled={ pending }
className={ styles.spacing }
label={ action === 'reserve' ? 'Reserve' : 'Drop' }
primary
icon={ <CheckIcon /> }
onTouchTap={ this.onSubmitClick }
/>
</div>
{ queue.length > 0
? (<div>{ useSignerText }{ renderQueue(queue) }</div>)
: null
@ -156,9 +163,14 @@ export default class Names extends Component {
onSubmitClick = () => {
const { action, name } = this.state;
if (action === 'reserve') {
this.props.actions.reserve(name);
this.props.reserve(name);
} 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
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions';
const initialState = {
pending: false,
queue: []
};
export default (state = initialState, action) => {
if (action.type === 'names reserve start') {
return { ...state, pending: true };
}
if (action.type === 'names reserve success') {
return {
...state, pending: false,
queue: state.queue.concat({ action: 'reserve', name: action.name })
};
}
if (action.type === 'names reserve fail') {
return { ...state, pending: false };
}
if (action.type === 'names drop start') {
return { ...state, pending: true };
}
if (action.type === 'names drop success') {
return {
...state, pending: false,
queue: state.queue.concat({ action: 'drop', name: action.name })
};
}
if (action.type === 'names drop fail') {
return { ...state, pending: false };
if (isAction('names', 'reserve', action)) {
if (isStage('start', action)) {
return {
...state, pending: true,
queue: addToQueue(state.queue, 'reserve', action.name)
};
} else if (isStage('success', action) || isStage('fail', action)) {
return {
...state, pending: false,
queue: removeFromQueue(state.queue, 'reserve', action.name)
};
}
} else if (isAction('names', 'drop', action)) {
if (isStage('start', action)) {
return {
...state, pending: true,
queue: addToQueue(state.queue, 'drop', action.name)
};
} else if (isStage('success', action) || isStage('fail', action)) {
return {
...state, pending: false,
queue: removeFromQueue(state.queue, 'drop', action.name)
};
}
}
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 });
@ -10,25 +27,28 @@ export const update = (name, key, value) => (dispatch, getState) => {
const state = getState();
const account = state.accounts.selected;
const contract = state.contract;
if (!contract || !account) {
return;
}
const fnName = key === 'A' ? 'setAddress' : 'set';
const fn = contract.functions.find((f) => f.name === fnName);
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));
fn.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return fn.postTransaction(options, values);
})
.then((data) => {
const options = {
from: account.address
};
const values = [
sha3(name),
key,
value
];
postTx(api, setAddress, options, values)
.then((txHash) => {
dispatch(success());
}).catch((err) => {
console.error(`could not update ${key} record of ${name}`);

View File

@ -23,6 +23,16 @@
margin-top: 0;
}
.box {
display: flex;
align-items: baseline;
}
.spacing {
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/>.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Card, CardHeader, CardText } from 'material-ui/Card';
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 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';
export default class Records extends Component {
class Records extends Component {
static propTypes = {
actions: PropTypes.object.isRequired,
pending: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
value: PropTypes.string.isRequired,
update: PropTypes.func.isRequired
}
state = { name: '', type: 'A', value: '' };
@ -48,28 +53,36 @@ export default class Records extends Component {
<p className={ styles.noSpacing }>
You can only modify entries of names that you previously registered.
</p>
<TextField
className={ styles.spacing }
hintText='name'
value={ name }
onChange={ this.onNameChange }
/>
{ recordTypeSelect(type, this.onTypeChange, styles.spacing) }
<TextField
className={ styles.spacing }
hintText='value'
value={ value }
onChange={ this.onValueChange }
/>
<RaisedButton
disabled={ pending }
className={ styles.spacing }
label='Save'
primary
icon={ <SaveIcon /> }
onTouchTap={ this.onSaveClick }
/>
<div className={ styles.box }>
<TextField
hintText='name'
value={ name }
onChange={ this.onNameChange }
/>
<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' />
</DropDownMenu>
<TextField
hintText='value'
value={ value }
onChange={ this.onValueChange }
/>
<div className={ styles.button }>
<RaisedButton
disabled={ pending }
className={ styles.spacing }
label='Save'
primary
icon={ <SaveIcon /> }
onTouchTap={ this.onSaveClick }
/>
</div>
</div>
</CardText>
</Card>
);
@ -86,6 +99,11 @@ export default class Records extends Component {
};
onSaveClick = () => {
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 = {
pending: false,
name: '', type: '', value: ''
};
export default (state = initialState, action) => {
if (action.type === 'records update start') {
return {
...state,
pending: true,
name: action.name, type: action.entry, value: action.value
};
if (!isAction('records', 'update', action)) {
return state;
}
if (action.type === 'records update error' || action.type === 'records update success') {
if (isStage('start', action)) {
return {
...state,
pending: false,
...state, pending: true,
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
};
}

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 names from './Names/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 fetchContract = () => (dispatch) =>
api.parity.registryAddress()
.then((address) => {
const contract = api.newContract(registryAbi, address);
dispatch(setContract(contract));
dispatch(fetchFee());
dispatch(fetchOwner());
})
.catch((err) => {
console.error('could not fetch contract');
if (err) {
console.error(err.stack);
}
});
.then((address) => {
const contract = api.newContract(registryAbi, address);
dispatch(setContract(contract));
dispatch(fetchFee());
dispatch(fetchOwner());
})
.catch((err) => {
console.error('could not fetch contract');
if (err) {
console.error(err.stack);
}
});
export const setFee = (fee) => ({ type: 'set fee', fee });
const fetchFee = () => (dispatch, getState) => {
const { contract } = getState();
if (!contract) {
return;
}
contract.instance.fee.call()
.then((fee) => dispatch(setFee(fee)))
.catch((err) => {
console.error('could not fetch fee');
if (err) {
console.error(err.stack);
}
});
.then((fee) => dispatch(setFee(fee)))
.catch((err) => {
console.error('could not fetch fee');
if (err) {
console.error(err.stack);
}
});
};
export const setOwner = (owner) => ({ type: 'set owner', owner });
export const fetchOwner = () => (dispatch, getState) => {
const { contract } = getState();
if (!contract) {
return;
}
contract.instance.owner.call()
.then((owner) => dispatch(setOwner(owner)))
.catch((err) => {
console.error('could not fetch owner');
if (err) {
console.error(err.stack);
}
});
.then((owner) => dispatch(setOwner(owner)))
.catch((err) => {
console.error('could not fetch owner');
if (err) {
console.error(err.stack);
}
});
};

View File

@ -20,6 +20,10 @@ import lookupReducer from './Lookup/reducers.js';
import eventsReducer from './Events/reducers.js';
import namesReducer from './Names/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) =>
action.type === 'set contract' ? action.contract : state;
@ -31,6 +35,7 @@ const ownerReducer = (state = null, action) =>
action.type === 'set owner' ? action.owner : state;
const initialState = {
isTestnet: isTestnetReducer(undefined, { type: '' }),
accounts: accountsReducer(undefined, { type: '' }),
contacts: contactsReducer(undefined, { type: '' }),
contract: contractReducer(undefined, { type: '' }),
@ -39,10 +44,12 @@ const initialState = {
lookup: lookupReducer(undefined, { type: '' }),
events: eventsReducer(undefined, { type: '' }),
names: namesReducer(undefined, { type: '' }),
records: recordsReducer(undefined, { type: '' })
records: recordsReducer(undefined, { type: '' }),
reverse: reverseReducer(undefined, { type: '' })
};
export default (state = initialState, action) => ({
isTestnet: isTestnetReducer(state.isTestnet, action),
accounts: accountsReducer(state.accounts, action),
contacts: contactsReducer(state.contacts, action),
contract: contractReducer(state.contract, action),
@ -51,5 +58,6 @@ export default (state = initialState, action) => ({
lookup: lookupReducer(state.lookup, action),
events: eventsReducer(state.events, 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
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import renderHash from './hash';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import Hash from './hash';
import etherscanUrl from '../util/etherscan-url';
import IdentityIcon from '../IdentityIcon';
const container = {
display: 'inline-block',
verticalAlign: 'middle',
height: '24px'
};
const align = {
display: 'inline-block',
verticalAlign: 'top',
lineHeight: '24px'
};
import styles from './address.css';
export default (address, accounts, contacts, shortenHash = true) => {
let caption;
if (accounts[address]) {
caption = (<abbr title={ address } style={ align }>{ accounts[address].name || address }</abbr>);
} else if (contacts[address]) {
caption = (<abbr title={ address } style={ align }>{ contacts[address].name || address }</abbr>);
} else {
caption = (<code style={ align }>{ shortenHash ? renderHash(address) : address }</code>);
class Address extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired,
isTestnet: PropTypes.bool.isRequired,
key: PropTypes.string,
shortenHash: PropTypes.bool
}
return (
<div style={ container }>
<IdentityIcon address={ address } style={ align } />
{ caption }
</div>
);
};
static defaultProps = {
key: 'address',
shortenHash: true
}
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
// 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) => {
const shortened = hash.length > (2 + 9 + 9)
? hash.substr(2, 9) + '...' + hash.slice(-9)
: hash.slice(2);
return (<abbr title={ hash }>{ shortened }</abbr>);
};
import etherscanUrl from '../util/etherscan-url';
import styles from './hash.css';
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
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem';
export const isAction = (ns, type, action) => {
return action.type.slice(0, ns.length + 1 + type.length) === `${ns} ${type}`;
};
export default (value, onSelect, className = '') => (
<DropDownMenu className={ className } value={ value } onChange={ onSelect }>
<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' />
</DropDownMenu>
);
export const isStage = (stage, action) => {
return action.type.slice(-1 - stage.length) === ` ${stage}`;
};
export const addToQueue = (queue, action, name) => {
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;