From 002e8b00d4969a5e5ebb268eea243ef93692ff49 Mon Sep 17 00:00:00 2001 From: Jannis Redmann Date: Tue, 27 Dec 2016 11:01:16 +0100 Subject: [PATCH] registry dapp: cleanup, support reverse entries (#3933) * style fixes :sparkles: * registry dapp: show reverse events * registry dapp: actions & reducers for isTestnet * registry dapp: make Hash & Address components * registry dapp: code style :sparkles: * registry dapp: bugfixes :bug: * 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 :lipstick: * registry dapp: address style grumbles :lipstick: --- js/src/dapps/registry/Accounts/accounts.js | 42 +++-- .../dapps/registry/Application/application.js | 40 ++--- js/src/dapps/registry/Container.js | 1 + js/src/dapps/registry/Events/actions.js | 9 +- js/src/dapps/registry/Events/events.css | 6 + js/src/dapps/registry/Events/events.js | 168 ++++++++++++++---- js/src/dapps/registry/Events/reducers.js | 10 +- js/src/dapps/registry/Lookup/actions.js | 41 +++-- js/src/dapps/registry/Lookup/lookup.css | 6 +- js/src/dapps/registry/Lookup/lookup.js | 93 +++++++--- js/src/dapps/registry/Lookup/reducers.js | 37 ++-- js/src/dapps/registry/Names/actions.js | 56 +++--- js/src/dapps/registry/Names/names.css | 3 +- js/src/dapps/registry/Names/names.js | 64 ++++--- js/src/dapps/registry/Names/reducers.js | 50 +++--- js/src/dapps/registry/Records/actions.js | 46 +++-- js/src/dapps/registry/Records/records.css | 10 ++ js/src/dapps/registry/Records/records.js | 72 +++++--- js/src/dapps/registry/Records/reducers.js | 36 +++- js/src/dapps/registry/Reverse/actions.js | 92 ++++++++++ js/src/dapps/registry/Reverse/index.js | 1 + js/src/dapps/registry/Reverse/reducers.js | 68 +++++++ js/src/dapps/registry/Reverse/reverse.css | 39 ++++ js/src/dapps/registry/Reverse/reverse.js | 136 ++++++++++++++ js/src/dapps/registry/actions.js | 77 ++++---- js/src/dapps/registry/reducers.js | 12 +- js/src/dapps/registry/ui/address.css | 41 +++++ js/src/dapps/registry/ui/address.js | 105 ++++++++--- js/src/dapps/registry/ui/hash.css | 25 +++ js/src/dapps/registry/ui/hash.js | 56 +++++- .../record-type-select.js => util/actions.js} | 24 +-- js/src/dapps/registry/util/etherscan-url.js | 26 +++ js/src/dapps/registry/util/post-tx.js | 36 ++++ 33 files changed, 1177 insertions(+), 351 deletions(-) create mode 100644 js/src/dapps/registry/Reverse/actions.js create mode 100644 js/src/dapps/registry/Reverse/index.js create mode 100644 js/src/dapps/registry/Reverse/reducers.js create mode 100644 js/src/dapps/registry/Reverse/reverse.css create mode 100644 js/src/dapps/registry/Reverse/reverse.js create mode 100644 js/src/dapps/registry/ui/address.css create mode 100644 js/src/dapps/registry/ui/hash.css rename js/src/dapps/registry/{ui/record-type-select.js => util/actions.js} (57%) create mode 100644 js/src/dapps/registry/util/etherscan-url.js create mode 100644 js/src/dapps/registry/util/post-tx.js diff --git a/js/src/dapps/registry/Accounts/accounts.js b/js/src/dapps/registry/Accounts/accounts.js index 076a8bfa4..29bcb7add 100644 --- a/js/src/dapps/registry/Accounts/accounts.js +++ b/js/src/dapps/registry/Accounts/accounts.js @@ -15,22 +15,26 @@ // along with Parity. If not, see . 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 = ( { selected - ? () - : () + ? ( + + ) : ( + + ) } ); @@ -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 ( - { renderAddress(account.address, all, {}) } +
); }; 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); diff --git a/js/src/dapps/registry/Application/application.js b/js/src/dapps/registry/Application/application.js index 0d1f7e50a..abfacc497 100644 --- a/js/src/dapps/registry/Application/application.js +++ b/js/src/dapps/registry/Application/application.js @@ -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 }

RΞgistry

- +
{ contract && fee ? (
- + { this.renderActions() } - +
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) }ETH is required for all registrations.
@@ -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 (
- - + + +
); } diff --git a/js/src/dapps/registry/Container.js b/js/src/dapps/registry/Container.js index 2de041126..3d5cf1dfe 100644 --- a/js/src/dapps/registry/Container.js +++ b/js/src/dapps/registry/Container.js @@ -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(() => { diff --git a/js/src/dapps/registry/Events/actions.js b/js/src/dapps/registry/Events/actions.js index 38a61bf12..6303b1bc5 100644 --- a/js/src/dapps/registry/Events/actions.js +++ b/js/src/dapps/registry/Events/actions.js @@ -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 }; diff --git a/js/src/dapps/registry/Events/events.css b/js/src/dapps/registry/Events/events.css index 58ad75c01..15a23acf8 100644 --- a/js/src/dapps/registry/Events/events.css +++ b/js/src/dapps/registry/Events/events.css @@ -49,3 +49,9 @@ .eventsList td { padding: 0.25em 0.5em; } + +.inline { + display: inline-block; + width: auto; + margin-right: 1em; +} diff --git a/js/src/dapps/registry/Events/events.js b/js/src/dapps/registry/Events/events.js index 77a985def..d204822d1 100644 --- a/js/src/dapps/registry/Events/events.js +++ b/js/src/dapps/registry/Events/events.js @@ -15,13 +15,17 @@ // along with Parity. If not, see . 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 ( - { renderAddress(e.parameters.owner.value, accounts, contacts) } - { verb } - { renderHash(bytesToHex(e.parameters.name.value)) } - { renderStatus(e.timestamp, e.state === 'pending') } + +
+ + + { verb } + + + + + + + + { renderStatus(e.timestamp, e.state === 'pending') } + ); }; -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 ( - { renderAddress(e.parameters.owner.value, accounts, contacts) } - updated - key { new Buffer(e.parameters.plainKey.value).toString('utf8') } of { renderHash(bytesToHex(e.parameters.name.value)) } +
+ + + updated + + + { 'key ' } + + { new Buffer(e.parameters.plainKey.value).toString('utf8') } + + { 'of ' } + + + + + + { renderStatus(e.timestamp, e.state === 'pending') } + + + ); +}; + +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 ( + + +
+ + { verb } + + { 'name ' } + { bytesToHex(e.parameters.name.value) } + { ' for ' } +
+ + + { renderStatus(e.timestamp, e.state === 'pending') } - { renderStatus(e.timestamp, e.state === 'pending') } ); }; @@ -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 ( @@ -149,6 +224,13 @@ export default class Events extends Component { onToggle={ this.onDataChangedToggle } style={ inlineButton } /> + @@ -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); diff --git a/js/src/dapps/registry/Events/reducers.js b/js/src/dapps/registry/Events/reducers.js index 70d332fb7..7be492bbb 100644 --- a/js/src/dapps/registry/Events/reducers.js +++ b/js/src/dapps/registry/Events/reducers.js @@ -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: [] }; diff --git a/js/src/dapps/registry/Lookup/actions.js b/js/src/dapps/registry/Lookup/actions.js index 8c8434cab..eb1c7db66 100644 --- a/js/src/dapps/registry/Lookup/actions.js +++ b/js/src/dapps/registry/Lookup/actions.js @@ -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')); }); }; diff --git a/js/src/dapps/registry/Lookup/lookup.css b/js/src/dapps/registry/Lookup/lookup.css index 66b4dc6a3..e415ab5f3 100644 --- a/js/src/dapps/registry/Lookup/lookup.css +++ b/js/src/dapps/registry/Lookup/lookup.css @@ -20,9 +20,7 @@ } .box { + display: flex; margin: 0 1em; -} - -.spacing { - margin-left: 1em; + align-items: baseline; } diff --git a/js/src/dapps/registry/Lookup/lookup.js b/js/src/dapps/registry/Lookup/lookup.js index a743219f2..bf01df115 100644 --- a/js/src/dapps/registry/Lookup/lookup.js +++ b/js/src/dapps/registry/Lookup/lookup.js @@ -15,50 +15,65 @@ // along with Parity. If not, see . 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 = ({ renderAddress(result, accounts, contacts, false) }); + output = ( + +
+ + ); } else if (type === 'IMG') { output = renderImage(result); } else if (type === 'CONTENT') { - output = (
- { result } -

This is most likely just the hash of the content you are looking for

-
); + output = ( +
+ { result } +

Keep in mind that this is most likely the hash of the content you are looking for.

+
+ ); } else { - output = ({ result }); + output = ( + { result } + ); } } @@ -67,14 +82,20 @@ export default class Lookup extends Component {
- { recordTypeSelect(type, this.onTypeChange, styles.spacing) } + + + + + + } @@ -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); diff --git a/js/src/dapps/registry/Lookup/reducers.js b/js/src/dapps/registry/Lookup/reducers.js index d6807c713..b675fc702 100644 --- a/js/src/dapps/registry/Lookup/reducers.js +++ b/js/src/dapps/registry/Lookup/reducers.js @@ -14,39 +14,34 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +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; diff --git a/js/src/dapps/registry/Names/actions.js b/js/src/dapps/registry/Names/actions.js index 448f710b1..67867ca8e 100644 --- a/js/src/dapps/registry/Names/actions.js +++ b/js/src/dapps/registry/Names/actions.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . 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) { diff --git a/js/src/dapps/registry/Names/names.css b/js/src/dapps/registry/Names/names.css index 4507e38d4..b56387909 100644 --- a/js/src/dapps/registry/Names/names.css +++ b/js/src/dapps/registry/Names/names.css @@ -20,7 +20,8 @@ } .box { - margin: 0 1em 1em 1em; + display: flex; + align-items: baseline; } .spacing { diff --git a/js/src/dapps/registry/Names/names.js b/js/src/dapps/registry/Names/names.js index c0c4badf0..c3f0e79f6 100644 --- a/js/src/dapps/registry/Names/names.js +++ b/js/src/dapps/registry/Names/names.js @@ -15,6 +15,8 @@ // along with Parity. If not, see . 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 = (

Use the Signer to authenticate the following changes.

); @@ -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 { : (

To drop a name, you have to be the owner.

) ) } - - - - - - } - onTouchTap={ this.onSubmitClick } - /> +
+ + + + + + } + onTouchTap={ this.onSubmitClick } + /> +
{ queue.length > 0 ? (
{ useSignerText }{ renderQueue(queue) }
) : 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); diff --git a/js/src/dapps/registry/Names/reducers.js b/js/src/dapps/registry/Names/reducers.js index 2b43c8b3e..17230ad40 100644 --- a/js/src/dapps/registry/Names/reducers.js +++ b/js/src/dapps/registry/Names/reducers.js @@ -14,36 +14,38 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +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; diff --git a/js/src/dapps/registry/Records/actions.js b/js/src/dapps/registry/Records/actions.js index cff54d94d..9afcb172c 100644 --- a/js/src/dapps/registry/Records/actions.js +++ b/js/src/dapps/registry/Records/actions.js @@ -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 . + +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}`); diff --git a/js/src/dapps/registry/Records/records.css b/js/src/dapps/registry/Records/records.css index a2224125a..e16ea4a15 100644 --- a/js/src/dapps/registry/Records/records.css +++ b/js/src/dapps/registry/Records/records.css @@ -23,6 +23,16 @@ margin-top: 0; } +.box { + display: flex; + align-items: baseline; +} + .spacing { margin-left: 1em; } + +.button { + flex-grow: 0; + flex-shrink: 0; +} diff --git a/js/src/dapps/registry/Records/records.js b/js/src/dapps/registry/Records/records.js index 085a70152..f1d92cac8 100644 --- a/js/src/dapps/registry/Records/records.js +++ b/js/src/dapps/registry/Records/records.js @@ -15,22 +15,27 @@ // along with Parity. If not, see . 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 {

You can only modify entries of names that you previously registered.

- - - { recordTypeSelect(type, this.onTypeChange, styles.spacing) } - - } - onTouchTap={ this.onSaveClick } - /> +
+ + + + + + + +
+ } + onTouchTap={ this.onSaveClick } + /> +
+
); @@ -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); diff --git a/js/src/dapps/registry/Records/reducers.js b/js/src/dapps/registry/Records/reducers.js index 7e8f06755..9629e8149 100644 --- a/js/src/dapps/registry/Records/reducers.js +++ b/js/src/dapps/registry/Records/reducers.js @@ -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 . + +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 }; } diff --git a/js/src/dapps/registry/Reverse/actions.js b/js/src/dapps/registry/Reverse/actions.js new file mode 100644 index 000000000..07a1afade --- /dev/null +++ b/js/src/dapps/registry/Reverse/actions.js @@ -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 . + +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')); + }); +}; diff --git a/js/src/dapps/registry/Reverse/index.js b/js/src/dapps/registry/Reverse/index.js new file mode 100644 index 000000000..ecaf9c3db --- /dev/null +++ b/js/src/dapps/registry/Reverse/index.js @@ -0,0 +1 @@ +export default from './reverse'; diff --git a/js/src/dapps/registry/Reverse/reducers.js b/js/src/dapps/registry/Reverse/reducers.js new file mode 100644 index 000000000..53a242c3b --- /dev/null +++ b/js/src/dapps/registry/Reverse/reducers.js @@ -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 . + +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; +}; diff --git a/js/src/dapps/registry/Reverse/reverse.css b/js/src/dapps/registry/Reverse/reverse.css new file mode 100644 index 000000000..0b75bfaf4 --- /dev/null +++ b/js/src/dapps/registry/Reverse/reverse.css @@ -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 . +*/ + +.reverse { + margin: 1em; +} + +.noSpacing { + margin-top: 0; +} + +.box { + display: flex; + align-items: baseline; +} + +.spacing { + margin-right: 1em; +} + +.button { + flex-grow: 0; + flex-shrink: 0; +} + diff --git a/js/src/dapps/registry/Reverse/reverse.js b/js/src/dapps/registry/Reverse/reverse.js new file mode 100644 index 000000000..79bf2b07f --- /dev/null +++ b/js/src/dapps/registry/Reverse/reverse.js @@ -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 . + +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' + ? ( +

+ To propose a reverse entry for foo, you have to be the owner of it. +

+ ) : ( +

+ To confirm a proposal, send the transaction from the account that the name has been proposed for. +

+ ); + + let addressInput = null; + if (action === 'propose') { + addressInput = ( + + ); + } + + return ( + + + +

+ + To make others to find the name of an address using the registry, you can propose & confirm reverse entries. + +

+ { explanation } +
+ + + + + { addressInput } + +
+ : } + onTouchTap={ this.onSubmitClick } + /> +
+
+
+
+ ); + } + + 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); diff --git a/js/src/dapps/registry/actions.js b/js/src/dapps/registry/actions.js index 391953c22..6f4cef8e9 100644 --- a/js/src/dapps/registry/actions.js +++ b/js/src/dapps/registry/actions.js @@ -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); + } + }); }; diff --git a/js/src/dapps/registry/reducers.js b/js/src/dapps/registry/reducers.js index 06b8f024b..45ca9642e 100644 --- a/js/src/dapps/registry/reducers.js +++ b/js/src/dapps/registry/reducers.js @@ -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) }); diff --git a/js/src/dapps/registry/ui/address.css b/js/src/dapps/registry/ui/address.css new file mode 100644 index 000000000..69cd27b9e --- /dev/null +++ b/js/src/dapps/registry/ui/address.css @@ -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 . +*/ + +.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; + } +} diff --git a/js/src/dapps/registry/ui/address.js b/js/src/dapps/registry/ui/address.js index f19894b48..e3eac2c97 100644 --- a/js/src/dapps/registry/ui/address.js +++ b/js/src/dapps/registry/ui/address.js @@ -14,34 +14,85 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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 = ({ accounts[address].name || address }); - } else if (contacts[address]) { - caption = ({ contacts[address].name || address }); - } else { - caption = ({ shortenHash ? renderHash(address) : address }); +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 ( -
- - { caption } -
- ); -}; + + 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 = ( + + + { name || address } + + + ); + } else { + caption = ( + + { shortenHash ? ( + + ) : address } + + ); + } + + return ( +
+ + { caption } +
+ ); + } +} + +export default connect( + // mapStateToProps + (state) => ({ + accounts: state.accounts.all, + contacts: state.contacts, + isTestnet: state.isTestnet + }), + // mapDispatchToProps + null +)(Address); diff --git a/js/src/dapps/registry/ui/hash.css b/js/src/dapps/registry/ui/hash.css new file mode 100644 index 000000000..a16d340a1 --- /dev/null +++ b/js/src/dapps/registry/ui/hash.css @@ -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 . +*/ + +.link { + text-decoration: none; + color: inherit; + + &:hover { + text-decoration: underline; + } +} diff --git a/js/src/dapps/registry/ui/hash.js b/js/src/dapps/registry/ui/hash.js index 7a586b256..6eeaab7b2 100644 --- a/js/src/dapps/registry/ui/hash.js +++ b/js/src/dapps/registry/ui/hash.js @@ -14,11 +14,53 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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 ({ shortened }); -}; +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 ( + + { shortened } + + ); + } + + return ({ shortened }); + } +} + +export default connect( + (state) => ({ // mapStateToProps + isTestnet: state.isTestnet + }), + null // mapDispatchToProps +)(Hash); diff --git a/js/src/dapps/registry/ui/record-type-select.js b/js/src/dapps/registry/util/actions.js similarity index 57% rename from js/src/dapps/registry/ui/record-type-select.js rename to js/src/dapps/registry/util/actions.js index e146dbf7d..0f4f350fc 100644 --- a/js/src/dapps/registry/ui/record-type-select.js +++ b/js/src/dapps/registry/util/actions.js @@ -14,14 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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 = '') => ( - - - - - -); +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); +}; diff --git a/js/src/dapps/registry/util/etherscan-url.js b/js/src/dapps/registry/util/etherscan-url.js new file mode 100644 index 000000000..26fb9a84f --- /dev/null +++ b/js/src/dapps/registry/util/etherscan-url.js @@ -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 . + +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; diff --git a/js/src/dapps/registry/util/post-tx.js b/js/src/dapps/registry/util/post-tx.js new file mode 100644 index 000000000..84326dcab --- /dev/null +++ b/js/src/dapps/registry/util/post-tx.js @@ -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 . + +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;