From 63017268ad2da404f24e95ec5b059d3b3375afbc Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 4 Jan 2017 15:14:37 +0100 Subject: [PATCH] Add ownership checks the Registry dApp (#4001) * Fixes to the Registry dApp * WIP Add Owner Lookup * Proper sha3 implementation * Add working owner lookup to reg dApp * Add errors to Name Reg * Add records error in Reg dApp * Add errors for reverse in reg dApp * PR Grumbles --- js/package.json | 1 + js/src/api/util/sha3.js | 19 ++- js/src/dapps/registry.js | 13 ++ .../dapps/registry/Application/application.js | 4 +- js/src/dapps/registry/Lookup/actions.js | 44 +++++-- js/src/dapps/registry/Lookup/lookup.js | 116 ++++++++++++------ js/src/dapps/registry/Lookup/reducers.js | 2 +- js/src/dapps/registry/Names/actions.js | 80 +++++++----- js/src/dapps/registry/Names/names.css | 9 +- js/src/dapps/registry/Names/names.js | 59 +++++---- js/src/dapps/registry/Names/reducers.js | 37 ++++-- js/src/dapps/registry/Records/actions.js | 46 ++++--- js/src/dapps/registry/Records/records.css | 4 + js/src/dapps/registry/Records/records.js | 32 ++++- js/src/dapps/registry/Records/reducers.js | 17 ++- js/src/dapps/registry/Reverse/actions.js | 77 ++++++++---- js/src/dapps/registry/Reverse/reducers.js | 21 +++- js/src/dapps/registry/Reverse/reverse.css | 3 + js/src/dapps/registry/Reverse/reverse.js | 32 ++++- js/src/dapps/registry/ui/address.js | 98 +++++++++------ js/src/dapps/registry/ui/image.js | 24 ++-- js/src/dapps/registry/util/actions.js | 4 +- js/src/dapps/registry/util/post-tx.js | 6 - js/src/dapps/registry/util/registry.js | 37 ++++++ .../transactionPendingFormConfirm.js | 5 +- js/src/views/Wallet/wallet.js | 2 +- 26 files changed, 574 insertions(+), 218 deletions(-) create mode 100644 js/src/dapps/registry/util/registry.js diff --git a/js/package.json b/js/package.json index 2ecad7269ed..8672578c74c 100644 --- a/js/package.json +++ b/js/package.json @@ -138,6 +138,7 @@ "blockies": "0.0.2", "brace": "0.9.0", "bytes": "2.4.0", + "crypto-js": "3.1.9-1", "debounce": "1.0.0", "es6-error": "4.0.0", "es6-promise": "4.0.5", diff --git a/js/src/api/util/sha3.js b/js/src/api/util/sha3.js index 93b01d8dd7a..5a2c7c27343 100644 --- a/js/src/api/util/sha3.js +++ b/js/src/api/util/sha3.js @@ -14,8 +14,21 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { keccak_256 } from 'js-sha3'; // eslint-disable-line camelcase +import CryptoJS from 'crypto-js'; +import CryptoSha3 from 'crypto-js/sha3'; -export function sha3 (value) { - return `0x${keccak_256(value)}`; +export function sha3 (value, options) { + if (options && options.encoding === 'hex') { + if (value.length > 2 && value.substr(0, 2) === '0x') { + value = value.substr(2); + } + + value = CryptoJS.enc.Hex.parse(value); + } + + const hash = CryptoSha3(value, { + outputLength: 256 + }).toString(); + + return `0x${hash}`; } diff --git a/js/src/dapps/registry.js b/js/src/dapps/registry.js index 0b8a8be5576..3723bd9fa53 100644 --- a/js/src/dapps/registry.js +++ b/js/src/dapps/registry.js @@ -34,3 +34,16 @@ ReactDOM.render( , document.querySelector('#container') ); + +if (module.hot) { + module.hot.accept('./registry/Container', () => { + require('./registry/Container'); + + ReactDOM.render( + + + , + document.querySelector('#container') + ); + }); +} diff --git a/js/src/dapps/registry/Application/application.js b/js/src/dapps/registry/Application/application.js index abfacc49755..a1d5c0ef22d 100644 --- a/js/src/dapps/registry/Application/application.js +++ b/js/src/dapps/registry/Application/application.js @@ -44,8 +44,8 @@ export default class Application extends Component { static propTypes = { accounts: PropTypes.object.isRequired, - contract: nullableProptype(PropTypes.object).isRequired, - fee: nullableProptype(PropTypes.object).isRequired + contract: nullableProptype(PropTypes.object.isRequired), + fee: nullableProptype(PropTypes.object.isRequired) }; render () { diff --git a/js/src/dapps/registry/Lookup/actions.js b/js/src/dapps/registry/Lookup/actions.js index eb1c7db6697..1e8ed5898d7 100644 --- a/js/src/dapps/registry/Lookup/actions.js +++ b/js/src/dapps/registry/Lookup/actions.js @@ -15,11 +15,13 @@ // along with Parity. If not, see . import { sha3 } from '../parity.js'; +import { getOwner } from '../util/registry'; export const clear = () => ({ type: 'lookup clear' }); export const lookupStart = (name, key) => ({ type: 'lookup start', name, key }); export const reverseLookupStart = (address) => ({ type: 'reverseLookup start', address }); +export const ownerLookupStart = (name) => ({ type: 'ownerLookup start', name }); export const success = (action, result) => ({ type: `${action} success`, result: result }); @@ -48,24 +50,50 @@ export const lookup = (name, key) => (dispatch, getState) => { }); }; -export const reverseLookup = (address) => (dispatch, getState) => { +export const reverseLookup = (lookupAddress) => (dispatch, getState) => { const { contract } = getState(); + if (!contract) { return; } - const reverse = contract.functions - .find((f) => f.name === 'reverse'); - - dispatch(reverseLookupStart(address)); + dispatch(reverseLookupStart(lookupAddress)); - reverse.call({}, [ address ]) - .then((address) => dispatch(success('reverseLookup', address))) + contract.instance + .reverse + .call({}, [ lookupAddress ]) + .then((address) => { + dispatch(success('reverseLookup', address)); + }) .catch((err) => { - console.error(`could not lookup reverse for ${address}`); + console.error(`could not lookup reverse for ${lookupAddress}`); if (err) { console.error(err.stack); } dispatch(fail('reverseLookup')); }); }; + +export const ownerLookup = (name) => (dispatch, getState) => { + const { contract } = getState(); + + if (!contract) { + return; + } + + dispatch(ownerLookupStart(name)); + + return getOwner(contract, name) + .then((owner) => { + dispatch(success('ownerLookup', owner)); + }) + .catch((err) => { + console.error(`could not lookup owner for ${name}`); + + if (err) { + console.error(err.stack); + } + + dispatch(fail('ownerLookup')); + }); +}; diff --git a/js/src/dapps/registry/Lookup/lookup.js b/js/src/dapps/registry/Lookup/lookup.js index bf01df115f4..f572cbb7d9b 100644 --- a/js/src/dapps/registry/Lookup/lookup.js +++ b/js/src/dapps/registry/Lookup/lookup.js @@ -23,13 +23,14 @@ 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 keycode from 'keycode'; import { nullableProptype } from '~/util/proptypes'; import Address from '../ui/address.js'; import renderImage from '../ui/image.js'; -import { clear, lookup, reverseLookup } from './actions'; +import { clear, lookup, ownerLookup, reverseLookup } from './actions'; import styles from './lookup.css'; class Lookup extends Component { @@ -39,6 +40,7 @@ class Lookup extends Component { clear: PropTypes.func.isRequired, lookup: PropTypes.func.isRequired, + ownerLookup: PropTypes.func.isRequired, reverseLookup: PropTypes.func.isRequired } @@ -50,33 +52,6 @@ class Lookup extends Component { const { input, type } = this.state; const { result } = this.props; - let output = ''; - if (result) { - if (type === 'A') { - output = ( - -
- - ); - } else if (type === 'IMG') { - output = renderImage(result); - } else if (type === 'CONTENT') { - output = ( -
- { result } -

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

-
- ); - } else { - output = ( - { result } - ); - } - } - return ( @@ -85,6 +60,7 @@ class Lookup extends Component { hintText={ type === 'reverse' ? 'address' : 'name' } value={ input } onChange={ this.onInputChange } + onKeyDown={ this.onKeyDown } /> + - { output } + + { this.renderOutput(type, result) } + ); } + renderOutput (type, result) { + if (result === null) { + return null; + } + + if (type === 'A') { + return ( + +
+ + ); + } + + if (type === 'owner') { + if (!result) { + return ( + Not reserved yet + ); + } + + return ( + +
+ + ); + } + + if (type === 'IMG') { + return renderImage(result); + } + + if (type === 'CONTENT') { + return ( +
+ { result } +

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

+
+ ); + } + + return ( + { result || 'No data' } + ); + } + onInputChange = (e) => { this.setState({ input: e.target.value }); - }; + } + + onKeyDown = (event) => { + const codeName = keycode(event); + + if (codeName !== 'enter') { + return; + } + + this.onLookupClick(); + } onTypeChange = (e, i, type) => { this.setState({ type }); this.props.clear(); - }; + } onLookupClick = () => { const { input, type } = this.state; if (type === 'reverse') { - this.props.reverseLookup(input); - } else { - this.props.lookup(input, type); + return this.props.reverseLookup(input); } - }; + + if (type === 'owner') { + return this.props.ownerLookup(input); + } + + return this.props.lookup(input, type); + } } const mapStateToProps = (state) => state.lookup; const mapDispatchToProps = (dispatch) => bindActionCreators({ - clear, lookup, reverseLookup + clear, lookup, ownerLookup, 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 b675fc70255..8c804c28e68 100644 --- a/js/src/dapps/registry/Lookup/reducers.js +++ b/js/src/dapps/registry/Lookup/reducers.js @@ -24,7 +24,7 @@ const initialState = { export default (state = initialState, action) => { const { type } = action; - if (type.slice(0, 7) !== 'lookup ' && type.slice(0, 14) !== 'reverseLookup ') { + if (!/^(lookup|reverseLookup|ownerLookup)/.test(type)) { return state; } diff --git a/js/src/dapps/registry/Names/actions.js b/js/src/dapps/registry/Names/actions.js index 67867ca8eba..2396278cbe4 100644 --- a/js/src/dapps/registry/Names/actions.js +++ b/js/src/dapps/registry/Names/actions.js @@ -15,8 +15,13 @@ // along with Parity. If not, see . import { sha3, api } from '../parity.js'; +import { getOwner, isOwned } from '../util/registry'; import postTx from '../util/post-tx'; +export const clearError = () => ({ + type: 'clearError' +}); + const alreadyQueued = (queue, action, name) => !!queue.find((entry) => entry.action === action && entry.name === name); @@ -24,13 +29,14 @@ export const reserveStart = (name) => ({ type: 'names reserve start', name }); export const reserveSuccess = (name) => ({ type: 'names reserve success', name }); -export const reserveFail = (name) => ({ type: 'names reserve fail', name }); +export const reserveFail = (name, error) => ({ type: 'names reserve fail', name, error }); export const reserve = (name) => (dispatch, getState) => { const state = getState(); const account = state.accounts.selected; const contract = state.contract; const fee = state.fee; + if (!contract || !account) { return; } @@ -40,27 +46,34 @@ export const reserve = (name) => (dispatch, getState) => { if (alreadyQueued(state.names.queue, 'reserve', name)) { return; } - const reserve = contract.functions.find((f) => f.name === 'reserve'); dispatch(reserveStart(name)); - const options = { - from: account.address, - value: fee - }; - const values = [ - sha3(name) - ]; + return isOwned(contract, name) + .then((owned) => { + if (owned) { + throw new Error(`"${name}" has already been reserved`); + } + + const { reserve } = contract.instance; + + const options = { + from: account.address, + value: fee + }; + const values = [ + sha3(name) + ]; - postTx(api, reserve, options, values) + return postTx(api, reserve, options, values); + }) .then((txHash) => { dispatch(reserveSuccess(name)); }) .catch((err) => { - console.error(`could not reserve ${name}`); - - if (err) { - console.error(err.stack); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error rerserving ${name}`, err); + return dispatch(reserveFail(name, err)); } dispatch(reserveFail(name)); @@ -71,43 +84,52 @@ export const dropStart = (name) => ({ type: 'names drop start', name }); export const dropSuccess = (name) => ({ type: 'names drop success', name }); -export const dropFail = (name) => ({ type: 'names drop fail', name }); +export const dropFail = (name, error) => ({ type: 'names drop fail', name, error }); 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'); - dispatch(dropStart(name)); - const options = { - from: account.address - }; - const values = [ - sha3(name) - ]; + return getOwner(contract, name) + .then((owner) => { + if (owner.toLowerCase() !== account.address.toLowerCase()) { + throw new Error(`you are not the owner of "${name}"`); + } + + const { drop } = contract.instance; + + const options = { + from: account.address + }; - postTx(api, drop, options, values) + const values = [ + sha3(name) + ]; + + return postTx(api, drop, options, values); + }) .then((txhash) => { dispatch(dropSuccess(name)); }) .catch((err) => { - console.error(`could not drop ${name}`); - - if (err) { - console.error(err.stack); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error dropping ${name}`, err); + return dispatch(dropFail(name, err)); } - dispatch(reserveFail(name)); + dispatch(dropFail(name)); }); }; diff --git a/js/src/dapps/registry/Names/names.css b/js/src/dapps/registry/Names/names.css index b56387909e9..46d6a5560b0 100644 --- a/js/src/dapps/registry/Names/names.css +++ b/js/src/dapps/registry/Names/names.css @@ -35,7 +35,12 @@ .link { color: #00BCD4; text-decoration: none; + + &:hover { + text-decoration: underline; + } } -.link:hover { - text-decoration: underline; + +.error { + color: red; } diff --git a/js/src/dapps/registry/Names/names.js b/js/src/dapps/registry/Names/names.js index c3f0e79f691..c34e172b9be 100644 --- a/js/src/dapps/registry/Names/names.js +++ b/js/src/dapps/registry/Names/names.js @@ -24,9 +24,10 @@ import MenuItem from 'material-ui/MenuItem'; import RaisedButton from 'material-ui/RaisedButton'; import CheckIcon from 'material-ui/svg-icons/navigation/check'; +import { nullableProptype } from '~/util/proptypes'; import { fromWei } from '../parity.js'; -import { reserve, drop } from './actions'; +import { clearError, reserve, drop } from './actions'; import styles from './names.css'; const useSignerText = (

Use the Signer to authenticate the following changes.

); @@ -78,35 +79,21 @@ const renderQueue = (queue) => { class Names extends Component { static propTypes = { + error: nullableProptype(PropTypes.object.isRequired), fee: PropTypes.object.isRequired, pending: PropTypes.bool.isRequired, queue: PropTypes.array.isRequired, + clearError: PropTypes.func.isRequired, reserve: PropTypes.func.isRequired, drop: PropTypes.func.isRequired - } + }; state = { action: 'reserve', name: '' }; - componentWillReceiveProps (nextProps) { - const nextQueue = nextProps.queue; - const prevQueue = this.props.queue; - - if (nextQueue.length > prevQueue.length) { - const newQueued = nextQueue[nextQueue.length - 1]; - const newName = newQueued.name; - - if (newName !== this.state.name) { - return; - } - - this.setState({ name: '' }); - } - } - render () { const { action, name } = this.state; const { fee, pending, queue } = this.props; @@ -122,6 +109,7 @@ class Names extends Component { : (

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

) ) } + { this.renderError() }
+ { error.message } +
+ ); + } + onNameChange = (e) => { + this.clearError(); this.setState({ name: e.target.value }); }; + onActionChange = (e, i, action) => { + this.clearError(); this.setState({ action }); }; + onSubmitClick = () => { const { action, name } = this.state; + if (action === 'reserve') { - this.props.reserve(name); - } else if (action === 'drop') { - this.props.drop(name); + return this.props.reserve(name); + } + + if (action === 'drop') { + return this.props.drop(name); + } + }; + + clearError = () => { + if (this.props.error) { + this.props.clearError(); } }; } const mapStateToProps = (state) => ({ ...state.names, fee: state.fee }); -const mapDispatchToProps = (dispatch) => bindActionCreators({ reserve, drop }, dispatch); +const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, 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 17230ad4042..461718a58e0 100644 --- a/js/src/dapps/registry/Names/reducers.js +++ b/js/src/dapps/registry/Names/reducers.js @@ -17,32 +17,55 @@ import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions'; const initialState = { + error: null, pending: false, queue: [] }; export default (state = initialState, action) => { + switch (action.type) { + case 'clearError': + return { + ...state, + error: null + }; + } + if (isAction('names', 'reserve', action)) { if (isStage('start', action)) { return { - ...state, pending: true, + ...state, + error: null, + pending: true, queue: addToQueue(state.queue, 'reserve', action.name) }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { - ...state, pending: false, + ...state, + error: action.error || null, + pending: false, queue: removeFromQueue(state.queue, 'reserve', action.name) }; } - } else if (isAction('names', 'drop', action)) { + } + + if (isAction('names', 'drop', action)) { if (isStage('start', action)) { return { - ...state, pending: true, + ...state, + error: null, + pending: true, queue: addToQueue(state.queue, 'drop', action.name) }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { - ...state, pending: false, + ...state, + error: action.error || null, + pending: false, queue: removeFromQueue(state.queue, 'drop', action.name) }; } diff --git a/js/src/dapps/registry/Records/actions.js b/js/src/dapps/registry/Records/actions.js index 9afcb172c6a..f85304d5fd4 100644 --- a/js/src/dapps/registry/Records/actions.js +++ b/js/src/dapps/registry/Records/actions.js @@ -16,45 +16,57 @@ import { sha3, api } from '../parity.js'; import postTx from '../util/post-tx'; +import { getOwner } from '../util/registry'; + +export const clearError = () => ({ + type: 'clearError' +}); export const start = (name, key, value) => ({ type: 'records update start', name, key, value }); export const success = () => ({ type: 'records update success' }); -export const fail = () => ({ type: 'records update error' }); +export const fail = (error) => ({ type: 'records update fail', error }); export const update = (name, key, value) => (dispatch, getState) => { const state = getState(); const account = state.accounts.selected; const contract = state.contract; + if (!contract || !account) { return; } name = name.toLowerCase(); + dispatch(start(name, key, value)); - const fnName = key === 'A' ? 'setAddress' : 'set'; - const setAddress = contract.functions.find((f) => f.name === fnName); + return getOwner(contract, name) + .then((owner) => { + if (owner.toLowerCase() !== account.address.toLowerCase()) { + throw new Error(`you are not the owner of "${name}"`); + } - dispatch(start(name, key, value)); + const fnName = key === 'A' ? 'setAddress' : 'set'; + const method = contract.instance[fnName]; + + const options = { + from: account.address + }; - const options = { - from: account.address - }; - const values = [ - sha3(name), - key, - value - ]; + const values = [ + sha3(name), + key, + value + ]; - postTx(api, setAddress, options, values) + return postTx(api, method, options, values); + }) .then((txHash) => { dispatch(success()); }).catch((err) => { - console.error(`could not update ${key} record of ${name}`); - - if (err) { - console.error(err.stack); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error updating ${name}`, err); + return dispatch(fail(err)); } dispatch(fail()); diff --git a/js/src/dapps/registry/Records/records.css b/js/src/dapps/registry/Records/records.css index e16ea4a15bd..03af5801f05 100644 --- a/js/src/dapps/registry/Records/records.css +++ b/js/src/dapps/registry/Records/records.css @@ -36,3 +36,7 @@ flex-grow: 0; flex-shrink: 0; } + +.error { + color: red; +} diff --git a/js/src/dapps/registry/Records/records.js b/js/src/dapps/registry/Records/records.js index f1d92cac81b..f9c9cea7631 100644 --- a/js/src/dapps/registry/Records/records.js +++ b/js/src/dapps/registry/Records/records.js @@ -24,17 +24,20 @@ import MenuItem from 'material-ui/MenuItem'; import RaisedButton from 'material-ui/RaisedButton'; import SaveIcon from 'material-ui/svg-icons/content/save'; -import { update } from './actions'; +import { nullableProptype } from '~/util/proptypes'; +import { clearError, update } from './actions'; import styles from './records.css'; class Records extends Component { static propTypes = { + error: nullableProptype(PropTypes.object.isRequired), pending: PropTypes.bool.isRequired, name: PropTypes.string.isRequired, type: PropTypes.string.isRequired, value: PropTypes.string.isRequired, + clearError: PropTypes.func.isRequired, update: PropTypes.func.isRequired } @@ -53,6 +56,7 @@ class Records extends Component {

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

+ { this.renderError() }
+ { error.message } +
+ ); + } + onNameChange = (e) => { + this.clearError(); this.setState({ name: e.target.value }); }; + onTypeChange = (e, i, type) => { this.setState({ type }); }; + onValueChange = (e) => { this.setState({ value: e.target.value }); }; + onSaveClick = () => { const { name, type, value } = this.state; this.props.update(name, type, value); }; + + clearError = () => { + if (this.props.error) { + this.props.clearError(); + } + }; } const mapStateToProps = (state) => state.records; -const mapDispatchToProps = (dispatch) => bindActionCreators({ update }, dispatch); +const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, 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 9629e814960..2dd45c0120d 100644 --- a/js/src/dapps/registry/Records/reducers.js +++ b/js/src/dapps/registry/Records/reducers.js @@ -17,11 +17,20 @@ import { isAction, isStage } from '../util/actions'; const initialState = { + error: null, pending: false, name: '', type: '', value: '' }; export default (state = initialState, action) => { + switch (action.type) { + case 'clearError': + return { + ...state, + error: null + }; + } + if (!isAction('records', 'update', action)) { return state; } @@ -29,11 +38,15 @@ export default (state = initialState, action) => { if (isStage('start', action)) { return { ...state, pending: true, - name: action.name, type: action.entry, value: action.value + error: null, + name: action.name, type: action.key, value: action.value }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { ...state, pending: false, + error: action.error || null, 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 index 07a1afade82..bccd60f2faf 100644 --- a/js/src/dapps/registry/Reverse/actions.js +++ b/js/src/dapps/registry/Reverse/actions.js @@ -16,44 +16,58 @@ import { api } from '../parity.js'; import postTx from '../util/post-tx'; +import { getOwner } from '../util/registry'; + +export const clearError = () => ({ + type: 'clearError' +}); 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 fail = (action, error) => ({ type: `reverse ${action} fail`, 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(); + dispatch(start('propose', name, address)); - const proposeReverse = contract.functions.find((f) => f.name === 'proposeReverse'); + return getOwner(contract, name) + .then((owner) => { + if (owner.toLowerCase() !== account.address.toLowerCase()) { + throw new Error(`you are not the owner of "${name}"`); + } - dispatch(start('propose', name, address)); + const { proposeReverse } = contract.instance; - const options = { - from: account.address - }; - const values = [ - name, - address - ]; + const options = { + from: account.address + }; - postTx(api, proposeReverse, options, values) + const values = [ + name, + address + ]; + + return 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); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error proposing ${name}`, err); + return dispatch(fail('propose', err)); } + dispatch(fail('propose')); }); }; @@ -62,31 +76,42 @@ 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(); + dispatch(start('confirm', name)); - const confirmReverse = contract.functions.find((f) => f.name === 'confirmReverse'); + return getOwner(contract, name) + .then((owner) => { + if (owner.toLowerCase() !== account.address.toLowerCase()) { + throw new Error(`you are not the owner of "${name}"`); + } - dispatch(start('confirm', name)); + const { confirmReverse } = contract.instance; - const options = { - from: account.address - }; - const values = [ - name - ]; + const options = { + from: account.address + }; - postTx(api, confirmReverse, options, values) + const values = [ + name + ]; + + return 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); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error confirming ${name}`, err); + return dispatch(fail('confirm', err)); } + dispatch(fail('confirm')); }); }; + diff --git a/js/src/dapps/registry/Reverse/reducers.js b/js/src/dapps/registry/Reverse/reducers.js index 53a242c3bae..f7ba656480f 100644 --- a/js/src/dapps/registry/Reverse/reducers.js +++ b/js/src/dapps/registry/Reverse/reducers.js @@ -17,24 +17,37 @@ import { isAction, isStage } from '../util/actions'; const initialState = { + error: null, pending: false, queue: [] }; export default (state = initialState, action) => { + switch (action.type) { + case 'clearError': + return { + ...state, + error: null + }; + } + if (isAction('reverse', 'propose', action)) { if (isStage('start', action)) { return { ...state, pending: true, + error: null, queue: state.queue.concat({ action: 'propose', name: action.name, address: action.address }) }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { ...state, pending: false, + error: action.error || null, queue: state.queue.filter((e) => e.action === 'propose' && e.name === action.name && @@ -48,14 +61,18 @@ export default (state = initialState, action) => { if (isStage('start', action)) { return { ...state, pending: true, + error: null, queue: state.queue.concat({ action: 'confirm', name: action.name }) }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { ...state, pending: false, + error: action.error || null, queue: state.queue.filter((e) => e.action === 'confirm' && e.name === action.name diff --git a/js/src/dapps/registry/Reverse/reverse.css b/js/src/dapps/registry/Reverse/reverse.css index 0b75bfaf4f6..b7e5f64cb28 100644 --- a/js/src/dapps/registry/Reverse/reverse.css +++ b/js/src/dapps/registry/Reverse/reverse.css @@ -37,3 +37,6 @@ flex-shrink: 0; } +.error { + color: red; +} diff --git a/js/src/dapps/registry/Reverse/reverse.js b/js/src/dapps/registry/Reverse/reverse.js index 24af0a7a413..0216d00a20f 100644 --- a/js/src/dapps/registry/Reverse/reverse.js +++ b/js/src/dapps/registry/Reverse/reverse.js @@ -21,17 +21,20 @@ import { Card, CardHeader, CardText, TextField, DropDownMenu, MenuItem, RaisedButton } from 'material-ui'; +import { nullableProptype } from '~/util/proptypes'; import { AddIcon, CheckIcon } from '~/ui/Icons'; -import { propose, confirm } from './actions'; +import { clearError, confirm, propose } from './actions'; import styles from './reverse.css'; class Reverse extends Component { static propTypes = { + error: nullableProptype(PropTypes.object.isRequired), pending: PropTypes.bool.isRequired, queue: PropTypes.array.isRequired, - propose: PropTypes.func.isRequired, - confirm: PropTypes.func.isRequired + clearError: PropTypes.func.isRequired, + confirm: PropTypes.func.isRequired, + propose: PropTypes.func.isRequired } state = { @@ -77,6 +80,7 @@ class Reverse extends Component {

{ explanation } + { this.renderError() }
+ { error.message } +
+ ); + } + onNameChange = (e) => { this.setState({ name: e.target.value }); }; @@ -129,9 +147,15 @@ class Reverse extends Component { this.props.confirm(name); } }; + + clearError = () => { + if (this.props.error) { + this.props.clearError(); + } + }; } const mapStateToProps = (state) => state.reverse; -const mapDispatchToProps = (dispatch) => bindActionCreators({ propose, confirm }, dispatch); +const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, confirm, propose }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(Reverse); diff --git a/js/src/dapps/registry/ui/address.js b/js/src/dapps/registry/ui/address.js index e3eac2c977d..d8e98c220e8 100644 --- a/js/src/dapps/registry/ui/address.js +++ b/js/src/dapps/registry/ui/address.js @@ -20,31 +20,48 @@ import { connect } from 'react-redux'; import Hash from './hash'; import etherscanUrl from '../util/etherscan-url'; import IdentityIcon from '../IdentityIcon'; +import { nullableProptype } from '~/util/proptypes'; import styles from './address.css'; class Address extends Component { static propTypes = { address: PropTypes.string.isRequired, - accounts: PropTypes.object.isRequired, - contacts: PropTypes.object.isRequired, + account: nullableProptype(PropTypes.object.isRequired), isTestnet: PropTypes.bool.isRequired, key: PropTypes.string, shortenHash: PropTypes.bool - } + }; static defaultProps = { key: 'address', shortenHash: true - } + }; render () { - const { address, accounts, contacts, isTestnet, key, shortenHash } = this.props; + const { address, key } = this.props; + + return ( +
+ + { this.renderCaption() } +
+ ); + } + + renderCaption () { + const { address, account, isTestnet, shortenHash } = this.props; + + if (account) { + const { name } = account; - let caption; - if (accounts[address] || contacts[address]) { - const name = (accounts[address] || contacts[address] || {}).name; - caption = ( + return ( ); - } else { - caption = ( - - { shortenHash ? ( - - ) : address } - - ); } return ( -
- - { caption } -
+ + { shortenHash ? ( + + ) : address } + ); } } +function mapStateToProps (initState, initProps) { + const { accounts, contacts } = initState; + + const allAccounts = Object.assign({}, accounts.all, contacts); + + // Add lower case addresses to map + Object + .keys(allAccounts) + .forEach((address) => { + allAccounts[address.toLowerCase()] = allAccounts[address]; + }); + + return (state, props) => { + const { isTestnet } = state; + const { address = '' } = props; + + const account = allAccounts[address] || null; + + return { + account, + isTestnet + }; + }; +} + export default connect( - // mapStateToProps - (state) => ({ - accounts: state.accounts.all, - contacts: state.contacts, - isTestnet: state.isTestnet - }), - // mapDispatchToProps - null + mapStateToProps )(Address); diff --git a/js/src/dapps/registry/ui/image.js b/js/src/dapps/registry/ui/image.js index c66e3412801..c7774bfacff 100644 --- a/js/src/dapps/registry/ui/image.js +++ b/js/src/dapps/registry/ui/image.js @@ -23,10 +23,20 @@ const styles = { border: '1px solid #777' }; -export default (address) => ( - { -); +export default (address) => { + if (!address || /^(0x)?0*$/.test(address)) { + return ( + + No image + + ); + } + + return ( + { + ); +}; diff --git a/js/src/dapps/registry/util/actions.js b/js/src/dapps/registry/util/actions.js index 0f4f350fc5a..1ae7426de87 100644 --- a/js/src/dapps/registry/util/actions.js +++ b/js/src/dapps/registry/util/actions.js @@ -19,7 +19,7 @@ export const isAction = (ns, type, action) => { }; export const isStage = (stage, action) => { - return action.type.slice(-1 - stage.length) === ` ${stage}`; + return (new RegExp(`${stage}$`)).test(action.type); }; export const addToQueue = (queue, action, name) => { @@ -27,5 +27,5 @@ export const addToQueue = (queue, action, name) => { }; export const removeFromQueue = (queue, action, name) => { - return queue.filter((e) => e.action === action && e.name === name); + return queue.filter((e) => !(e.action === action && e.name === name)); }; diff --git a/js/src/dapps/registry/util/post-tx.js b/js/src/dapps/registry/util/post-tx.js index 84326dcab38..298bbd8436f 100644 --- a/js/src/dapps/registry/util/post-tx.js +++ b/js/src/dapps/registry/util/post-tx.js @@ -24,12 +24,6 @@ const postTx = (api, method, 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; }); }; diff --git a/js/src/dapps/registry/util/registry.js b/js/src/dapps/registry/util/registry.js new file mode 100644 index 00000000000..371b29aecb4 --- /dev/null +++ b/js/src/dapps/registry/util/registry.js @@ -0,0 +1,37 @@ +// 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 . + +export const getOwner = (contract, name) => { + const { address, api } = contract; + + const key = api.util.sha3(name) + '0000000000000000000000000000000000000000000000000000000000000001'; + const position = api.util.sha3(key, { encoding: 'hex' }); + + return api + .eth + .getStorageAt(address, position) + .then((result) => { + if (/^(0x)?0*$/.test(result)) { + return ''; + } + + return '0x' + result.slice(-40); + }); +}; + +export const isOwned = (contract, name) => { + return getOwner(contract, name).then((owner) => !!owner); +}; diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js index 02b7ef26603..99bd1c5f3e3 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js @@ -20,6 +20,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import RaisedButton from 'material-ui/RaisedButton'; import ReactTooltip from 'react-tooltip'; +import keycode from 'keycode'; import { Form, Input, IdentityIcon } from '~/ui'; @@ -207,7 +208,9 @@ class TransactionPendingFormConfirm extends Component { } onKeyDown = (event) => { - if (event.which !== 13) { + const codeName = keycode(event); + + if (codeName !== 'enter') { return; } diff --git a/js/src/views/Wallet/wallet.js b/js/src/views/Wallet/wallet.js index 5fe6c957ec1..5418448b41e 100644 --- a/js/src/views/Wallet/wallet.js +++ b/js/src/views/Wallet/wallet.js @@ -71,7 +71,7 @@ class Wallet extends Component { owned: PropTypes.bool.isRequired, setVisibleAccounts: PropTypes.func.isRequired, wallet: PropTypes.object.isRequired, - walletAccount: nullableProptype(PropTypes.object).isRequired + walletAccount: nullableProptype(PropTypes.object.isRequired) }; state = {