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
This commit is contained in:
Nicolas Gotchac 2017-01-04 15:14:37 +01:00 committed by Jaco Greeff
parent 4c532f9e3d
commit 63017268ad
26 changed files with 577 additions and 221 deletions

View File

@ -138,6 +138,7 @@
"blockies": "0.0.2", "blockies": "0.0.2",
"brace": "0.9.0", "brace": "0.9.0",
"bytes": "2.4.0", "bytes": "2.4.0",
"crypto-js": "3.1.9-1",
"debounce": "1.0.0", "debounce": "1.0.0",
"es6-error": "4.0.0", "es6-error": "4.0.0",
"es6-promise": "4.0.5", "es6-promise": "4.0.5",

View File

@ -14,8 +14,21 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { keccak_256 } from 'js-sha3'; // eslint-disable-line camelcase import CryptoJS from 'crypto-js';
import CryptoSha3 from 'crypto-js/sha3';
export function sha3 (value) { export function sha3 (value, options) {
return `0x${keccak_256(value)}`; 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}`;
} }

View File

@ -34,3 +34,16 @@ ReactDOM.render(
</Provider>, </Provider>,
document.querySelector('#container') document.querySelector('#container')
); );
if (module.hot) {
module.hot.accept('./registry/Container', () => {
require('./registry/Container');
ReactDOM.render(
<Provider store={ store }>
<Container />
</Provider>,
document.querySelector('#container')
);
});
}

View File

@ -44,8 +44,8 @@ export default class Application extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
contract: nullableProptype(PropTypes.object).isRequired, contract: nullableProptype(PropTypes.object.isRequired),
fee: nullableProptype(PropTypes.object).isRequired fee: nullableProptype(PropTypes.object.isRequired)
}; };
render () { render () {

View File

@ -15,11 +15,13 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { sha3 } from '../parity.js'; import { sha3 } from '../parity.js';
import { getOwner } from '../util/registry';
export const clear = () => ({ type: 'lookup clear' }); export const clear = () => ({ type: 'lookup clear' });
export const lookupStart = (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 reverseLookupStart = (address) => ({ type: 'reverseLookup start', address });
export const ownerLookupStart = (name) => ({ type: 'ownerLookup start', name });
export const success = (action, result) => ({ type: `${action} success`, result: result }); 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(); const { contract } = getState();
if (!contract) { if (!contract) {
return; return;
} }
const reverse = contract.functions dispatch(reverseLookupStart(lookupAddress));
.find((f) => f.name === 'reverse');
dispatch(reverseLookupStart(address)); contract.instance
.reverse
reverse.call({}, [ address ]) .call({}, [ lookupAddress ])
.then((address) => dispatch(success('reverseLookup', address))) .then((address) => {
dispatch(success('reverseLookup', address));
})
.catch((err) => { .catch((err) => {
console.error(`could not lookup reverse for ${address}`); console.error(`could not lookup reverse for ${lookupAddress}`);
if (err) { if (err) {
console.error(err.stack); console.error(err.stack);
} }
dispatch(fail('reverseLookup')); 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'));
});
};

View File

@ -23,13 +23,14 @@ import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem'; import MenuItem from 'material-ui/MenuItem';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import SearchIcon from 'material-ui/svg-icons/action/search'; import SearchIcon from 'material-ui/svg-icons/action/search';
import keycode from 'keycode';
import { nullableProptype } from '~/util/proptypes'; import { nullableProptype } from '~/util/proptypes';
import Address from '../ui/address.js'; import Address from '../ui/address.js';
import renderImage from '../ui/image.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'; import styles from './lookup.css';
class Lookup extends Component { class Lookup extends Component {
@ -39,6 +40,7 @@ class Lookup extends Component {
clear: PropTypes.func.isRequired, clear: PropTypes.func.isRequired,
lookup: PropTypes.func.isRequired, lookup: PropTypes.func.isRequired,
ownerLookup: PropTypes.func.isRequired,
reverseLookup: PropTypes.func.isRequired reverseLookup: PropTypes.func.isRequired
} }
@ -50,33 +52,6 @@ class Lookup extends Component {
const { input, type } = this.state; const { input, type } = this.state;
const { result } = this.props; const { result } = this.props;
let output = '';
if (result) {
if (type === 'A') {
output = (
<code>
<Address
address={ result }
shortenHash={ false }
/>
</code>
);
} else if (type === 'IMG') {
output = renderImage(result);
} else if (type === 'CONTENT') {
output = (
<div>
<code>{ result }</code>
<p>Keep in mind that this is most likely the hash of the content you are looking for.</p>
</div>
);
} else {
output = (
<code>{ result }</code>
);
}
}
return ( return (
<Card className={ styles.lookup }> <Card className={ styles.lookup }>
<CardHeader title={ 'Query the Registry' } /> <CardHeader title={ 'Query the Registry' } />
@ -85,6 +60,7 @@ class Lookup extends Component {
hintText={ type === 'reverse' ? 'address' : 'name' } hintText={ type === 'reverse' ? 'address' : 'name' }
value={ input } value={ input }
onChange={ this.onInputChange } onChange={ this.onInputChange }
onKeyDown={ this.onKeyDown }
/> />
<DropDownMenu <DropDownMenu
value={ type } value={ type }
@ -94,6 +70,7 @@ class Lookup extends Component {
<MenuItem value='IMG' primaryText='IMG  hash of a picture in the blockchain' /> <MenuItem value='IMG' primaryText='IMG  hash of a picture in the blockchain' />
<MenuItem value='CONTENT' primaryText='CONTENT  hash of a data in the blockchain' /> <MenuItem value='CONTENT' primaryText='CONTENT  hash of a data in the blockchain' />
<MenuItem value='reverse' primaryText='reverse find a name for an address' /> <MenuItem value='reverse' primaryText='reverse find a name for an address' />
<MenuItem value='owner' primaryText='owner find a the owner' />
</DropDownMenu> </DropDownMenu>
<RaisedButton <RaisedButton
label='Lookup' label='Lookup'
@ -102,35 +79,102 @@ class Lookup extends Component {
onTouchTap={ this.onLookupClick } onTouchTap={ this.onLookupClick }
/> />
</div> </div>
<CardText>{ output }</CardText> <CardText>
{ this.renderOutput(type, result) }
</CardText>
</Card> </Card>
); );
} }
renderOutput (type, result) {
if (result === null) {
return null;
}
if (type === 'A') {
return (
<code>
<Address
address={ result }
shortenHash={ false }
/>
</code>
);
}
if (type === 'owner') {
if (!result) {
return (
<code>Not reserved yet</code>
);
}
return (
<code>
<Address
address={ result }
shortenHash={ false }
/>
</code>
);
}
if (type === 'IMG') {
return renderImage(result);
}
if (type === 'CONTENT') {
return (
<div>
<code>{ result }</code>
<p>Keep in mind that this is most likely the hash of the content you are looking for.</p>
</div>
);
}
return (
<code>{ result || 'No data' }</code>
);
}
onInputChange = (e) => { onInputChange = (e) => {
this.setState({ input: e.target.value }); this.setState({ input: e.target.value });
}; }
onKeyDown = (event) => {
const codeName = keycode(event);
if (codeName !== 'enter') {
return;
}
this.onLookupClick();
}
onTypeChange = (e, i, type) => { onTypeChange = (e, i, type) => {
this.setState({ type }); this.setState({ type });
this.props.clear(); this.props.clear();
}; }
onLookupClick = () => { onLookupClick = () => {
const { input, type } = this.state; const { input, type } = this.state;
if (type === 'reverse') { if (type === 'reverse') {
this.props.reverseLookup(input); return this.props.reverseLookup(input);
} else { }
this.props.lookup(input, type);
if (type === 'owner') {
return this.props.ownerLookup(input);
}
return this.props.lookup(input, type);
} }
};
} }
const mapStateToProps = (state) => state.lookup; const mapStateToProps = (state) => state.lookup;
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
bindActionCreators({ bindActionCreators({
clear, lookup, reverseLookup clear, lookup, ownerLookup, reverseLookup
}, dispatch); }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Lookup); export default connect(mapStateToProps, mapDispatchToProps)(Lookup);

View File

@ -24,7 +24,7 @@ const initialState = {
export default (state = initialState, action) => { export default (state = initialState, action) => {
const { type } = action; const { type } = action;
if (type.slice(0, 7) !== 'lookup ' && type.slice(0, 14) !== 'reverseLookup ') { if (!/^(lookup|reverseLookup|ownerLookup)/.test(type)) {
return state; return state;
} }

View File

@ -15,8 +15,13 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { sha3, api } from '../parity.js'; import { sha3, api } from '../parity.js';
import { getOwner, isOwned } from '../util/registry';
import postTx from '../util/post-tx'; import postTx from '../util/post-tx';
export const clearError = () => ({
type: 'clearError'
});
const alreadyQueued = (queue, action, name) => const alreadyQueued = (queue, action, name) =>
!!queue.find((entry) => entry.action === action && entry.name === name); !!queue.find((entry) => entry.action === action && entry.name === name);
@ -24,13 +29,14 @@ export const reserveStart = (name) => ({ type: 'names reserve start', name });
export const reserveSuccess = (name) => ({ type: 'names reserve success', 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) => { export const reserve = (name) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
const fee = state.fee; const fee = state.fee;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
@ -40,10 +46,17 @@ export const reserve = (name) => (dispatch, getState) => {
if (alreadyQueued(state.names.queue, 'reserve', name)) { if (alreadyQueued(state.names.queue, 'reserve', name)) {
return; return;
} }
const reserve = contract.functions.find((f) => f.name === 'reserve');
dispatch(reserveStart(name)); dispatch(reserveStart(name));
return isOwned(contract, name)
.then((owned) => {
if (owned) {
throw new Error(`"${name}" has already been reserved`);
}
const { reserve } = contract.instance;
const options = { const options = {
from: account.address, from: account.address,
value: fee value: fee
@ -52,15 +65,15 @@ export const reserve = (name) => (dispatch, getState) => {
sha3(name) sha3(name)
]; ];
postTx(api, reserve, options, values) return postTx(api, reserve, options, values);
})
.then((txHash) => { .then((txHash) => {
dispatch(reserveSuccess(name)); dispatch(reserveSuccess(name));
}) })
.catch((err) => { .catch((err) => {
console.error(`could not reserve ${name}`); if (err.type !== 'REQUEST_REJECTED') {
console.error(`error rerserving ${name}`, err);
if (err) { return dispatch(reserveFail(name, err));
console.error(err.stack);
} }
dispatch(reserveFail(name)); 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 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) => { export const drop = (name) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase(); name = name.toLowerCase();
if (alreadyQueued(state.names.queue, 'drop', name)) { if (alreadyQueued(state.names.queue, 'drop', name)) {
return; return;
} }
const drop = contract.functions.find((f) => f.name === 'drop');
dispatch(dropStart(name)); dispatch(dropStart(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 = { const options = {
from: account.address from: account.address
}; };
const values = [ const values = [
sha3(name) sha3(name)
]; ];
postTx(api, drop, options, values) return postTx(api, drop, options, values);
})
.then((txhash) => { .then((txhash) => {
dispatch(dropSuccess(name)); dispatch(dropSuccess(name));
}) })
.catch((err) => { .catch((err) => {
console.error(`could not drop ${name}`); if (err.type !== 'REQUEST_REJECTED') {
console.error(`error dropping ${name}`, err);
if (err) { return dispatch(dropFail(name, err));
console.error(err.stack);
} }
dispatch(reserveFail(name)); dispatch(dropFail(name));
}); });
}; };

View File

@ -35,7 +35,12 @@
.link { .link {
color: #00BCD4; color: #00BCD4;
text-decoration: none; text-decoration: none;
}
.link:hover { &:hover {
text-decoration: underline; text-decoration: underline;
}
}
.error {
color: red;
} }

View File

@ -24,9 +24,10 @@ import MenuItem from 'material-ui/MenuItem';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import CheckIcon from 'material-ui/svg-icons/navigation/check'; import CheckIcon from 'material-ui/svg-icons/navigation/check';
import { nullableProptype } from '~/util/proptypes';
import { fromWei } from '../parity.js'; import { fromWei } from '../parity.js';
import { reserve, drop } from './actions'; import { clearError, reserve, drop } from './actions';
import styles from './names.css'; import styles from './names.css';
const useSignerText = (<p>Use the <a href='/#/signer' className={ styles.link } target='_blank'>Signer</a> to authenticate the following changes.</p>); const useSignerText = (<p>Use the <a href='/#/signer' className={ styles.link } target='_blank'>Signer</a> to authenticate the following changes.</p>);
@ -78,35 +79,21 @@ const renderQueue = (queue) => {
class Names extends Component { class Names extends Component {
static propTypes = { static propTypes = {
error: nullableProptype(PropTypes.object.isRequired),
fee: PropTypes.object.isRequired, fee: PropTypes.object.isRequired,
pending: PropTypes.bool.isRequired, pending: PropTypes.bool.isRequired,
queue: PropTypes.array.isRequired, queue: PropTypes.array.isRequired,
clearError: PropTypes.func.isRequired,
reserve: PropTypes.func.isRequired, reserve: PropTypes.func.isRequired,
drop: PropTypes.func.isRequired drop: PropTypes.func.isRequired
} };
state = { state = {
action: 'reserve', action: 'reserve',
name: '' 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 () { render () {
const { action, name } = this.state; const { action, name } = this.state;
const { fee, pending, queue } = this.props; const { fee, pending, queue } = this.props;
@ -122,6 +109,7 @@ class Names extends Component {
: (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>) : (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>)
) )
} }
{ this.renderError() }
<div className={ styles.box }> <div className={ styles.box }>
<TextField <TextField
hintText='name' hintText='name'
@ -154,23 +142,50 @@ class Names extends Component {
); );
} }
renderError () {
const { error } = this.props;
if (!error) {
return null;
}
return (
<div className={ styles.error }>
<code>{ error.message }</code>
</div>
);
}
onNameChange = (e) => { onNameChange = (e) => {
this.clearError();
this.setState({ name: e.target.value }); this.setState({ name: e.target.value });
}; };
onActionChange = (e, i, action) => { onActionChange = (e, i, action) => {
this.clearError();
this.setState({ action }); this.setState({ action });
}; };
onSubmitClick = () => { onSubmitClick = () => {
const { action, name } = this.state; const { action, name } = this.state;
if (action === 'reserve') { if (action === 'reserve') {
this.props.reserve(name); return this.props.reserve(name);
} else if (action === 'drop') { }
this.props.drop(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 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); export default connect(mapStateToProps, mapDispatchToProps)(Names);

View File

@ -17,32 +17,55 @@
import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions'; import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions';
const initialState = { const initialState = {
error: null,
pending: false, pending: false,
queue: [] queue: []
}; };
export default (state = initialState, action) => { export default (state = initialState, action) => {
switch (action.type) {
case 'clearError':
return {
...state,
error: null
};
}
if (isAction('names', 'reserve', action)) { if (isAction('names', 'reserve', action)) {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...state,
error: null,
pending: true,
queue: addToQueue(state.queue, 'reserve', action.name) queue: addToQueue(state.queue, 'reserve', action.name)
}; };
} else if (isStage('success', action) || isStage('fail', action)) { }
if (isStage('success', action) || isStage('fail', action)) {
return { return {
...state, pending: false, ...state,
error: action.error || null,
pending: false,
queue: removeFromQueue(state.queue, 'reserve', action.name) queue: removeFromQueue(state.queue, 'reserve', action.name)
}; };
} }
} else if (isAction('names', 'drop', action)) { }
if (isAction('names', 'drop', action)) {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...state,
error: null,
pending: true,
queue: addToQueue(state.queue, 'drop', action.name) queue: addToQueue(state.queue, 'drop', action.name)
}; };
} else if (isStage('success', action) || isStage('fail', action)) { }
if (isStage('success', action) || isStage('fail', action)) {
return { return {
...state, pending: false, ...state,
error: action.error || null,
pending: false,
queue: removeFromQueue(state.queue, 'drop', action.name) queue: removeFromQueue(state.queue, 'drop', action.name)
}; };
} }

View File

@ -16,45 +16,57 @@
import { sha3, api } from '../parity.js'; import { sha3, api } from '../parity.js';
import postTx from '../util/post-tx'; 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 start = (name, key, value) => ({ type: 'records update start', name, key, value });
export const success = () => ({ type: 'records update success' }); 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) => { export const update = (name, key, value) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase(); name = name.toLowerCase();
dispatch(start(name, key, value));
return getOwner(contract, name)
.then((owner) => {
if (owner.toLowerCase() !== account.address.toLowerCase()) {
throw new Error(`you are not the owner of "${name}"`);
}
const fnName = key === 'A' ? 'setAddress' : 'set'; const fnName = key === 'A' ? 'setAddress' : 'set';
const setAddress = contract.functions.find((f) => f.name === fnName); const method = contract.instance[fnName];
dispatch(start(name, key, value));
const options = { const options = {
from: account.address from: account.address
}; };
const values = [ const values = [
sha3(name), sha3(name),
key, key,
value value
]; ];
postTx(api, setAddress, options, values) return postTx(api, method, options, values);
})
.then((txHash) => { .then((txHash) => {
dispatch(success()); dispatch(success());
}).catch((err) => { }).catch((err) => {
console.error(`could not update ${key} record of ${name}`); if (err.type !== 'REQUEST_REJECTED') {
console.error(`error updating ${name}`, err);
if (err) { return dispatch(fail(err));
console.error(err.stack);
} }
dispatch(fail()); dispatch(fail());

View File

@ -36,3 +36,7 @@
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
} }
.error {
color: red;
}

View File

@ -24,17 +24,20 @@ import MenuItem from 'material-ui/MenuItem';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import SaveIcon from 'material-ui/svg-icons/content/save'; import SaveIcon from 'material-ui/svg-icons/content/save';
import { update } from './actions'; import { nullableProptype } from '~/util/proptypes';
import { clearError, update } from './actions';
import styles from './records.css'; import styles from './records.css';
class Records extends Component { class Records extends Component {
static propTypes = { static propTypes = {
error: nullableProptype(PropTypes.object.isRequired),
pending: PropTypes.bool.isRequired, pending: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
clearError: PropTypes.func.isRequired,
update: PropTypes.func.isRequired update: PropTypes.func.isRequired
} }
@ -53,6 +56,7 @@ class Records extends Component {
<p className={ styles.noSpacing }> <p className={ styles.noSpacing }>
You can only modify entries of names that you previously registered. You can only modify entries of names that you previously registered.
</p> </p>
{ this.renderError() }
<div className={ styles.box }> <div className={ styles.box }>
<TextField <TextField
hintText='name' hintText='name'
@ -88,22 +92,46 @@ class Records extends Component {
); );
} }
renderError () {
const { error } = this.props;
if (!error) {
return null;
}
return (
<div className={ styles.error }>
<code>{ error.message }</code>
</div>
);
}
onNameChange = (e) => { onNameChange = (e) => {
this.clearError();
this.setState({ name: e.target.value }); this.setState({ name: e.target.value });
}; };
onTypeChange = (e, i, type) => { onTypeChange = (e, i, type) => {
this.setState({ type }); this.setState({ type });
}; };
onValueChange = (e) => { onValueChange = (e) => {
this.setState({ value: e.target.value }); this.setState({ value: e.target.value });
}; };
onSaveClick = () => { onSaveClick = () => {
const { name, type, value } = this.state; const { name, type, value } = this.state;
this.props.update(name, type, value); this.props.update(name, type, value);
}; };
clearError = () => {
if (this.props.error) {
this.props.clearError();
}
};
} }
const mapStateToProps = (state) => state.records; const mapStateToProps = (state) => state.records;
const mapDispatchToProps = (dispatch) => bindActionCreators({ update }, dispatch); const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, update }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Records); export default connect(mapStateToProps, mapDispatchToProps)(Records);

View File

@ -17,11 +17,20 @@
import { isAction, isStage } from '../util/actions'; import { isAction, isStage } from '../util/actions';
const initialState = { const initialState = {
error: null,
pending: false, pending: false,
name: '', type: '', value: '' name: '', type: '', value: ''
}; };
export default (state = initialState, action) => { export default (state = initialState, action) => {
switch (action.type) {
case 'clearError':
return {
...state,
error: null
};
}
if (!isAction('records', 'update', action)) { if (!isAction('records', 'update', action)) {
return state; return state;
} }
@ -29,11 +38,15 @@ export default (state = initialState, action) => {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...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 { return {
...state, pending: false, ...state, pending: false,
error: action.error || null,
name: initialState.name, type: initialState.type, value: initialState.value name: initialState.name, type: initialState.type, value: initialState.value
}; };
} }

View File

@ -16,44 +16,58 @@
import { api } from '../parity.js'; import { api } from '../parity.js';
import postTx from '../util/post-tx'; 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 start = (action, name, address) => ({ type: `reverse ${action} start`, name, address });
export const success = (action) => ({ type: `reverse ${action} success` }); 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) => { export const propose = (name, address) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase(); name = name.toLowerCase();
const proposeReverse = contract.functions.find((f) => f.name === 'proposeReverse');
dispatch(start('propose', name, address)); dispatch(start('propose', name, address));
return getOwner(contract, name)
.then((owner) => {
if (owner.toLowerCase() !== account.address.toLowerCase()) {
throw new Error(`you are not the owner of "${name}"`);
}
const { proposeReverse } = contract.instance;
const options = { const options = {
from: account.address from: account.address
}; };
const values = [ const values = [
name, name,
address address
]; ];
postTx(api, proposeReverse, options, values) return postTx(api, proposeReverse, options, values);
})
.then((txHash) => { .then((txHash) => {
dispatch(success('propose')); dispatch(success('propose'));
}) })
.catch((err) => { .catch((err) => {
console.error(`could not propose reverse ${name} for address ${address}`); if (err.type !== 'REQUEST_REJECTED') {
if (err) { console.error(`error proposing ${name}`, err);
console.error(err.stack); return dispatch(fail('propose', err));
} }
dispatch(fail('propose')); dispatch(fail('propose'));
}); });
}; };
@ -62,31 +76,42 @@ export const confirm = (name) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase(); name = name.toLowerCase();
const confirmReverse = contract.functions.find((f) => f.name === 'confirmReverse');
dispatch(start('confirm', name)); dispatch(start('confirm', name));
return getOwner(contract, name)
.then((owner) => {
if (owner.toLowerCase() !== account.address.toLowerCase()) {
throw new Error(`you are not the owner of "${name}"`);
}
const { confirmReverse } = contract.instance;
const options = { const options = {
from: account.address from: account.address
}; };
const values = [ const values = [
name name
]; ];
postTx(api, confirmReverse, options, values) return postTx(api, confirmReverse, options, values);
})
.then((txHash) => { .then((txHash) => {
dispatch(success('confirm')); dispatch(success('confirm'));
}) })
.catch((err) => { .catch((err) => {
console.error(`could not confirm reverse ${name}`); if (err.type !== 'REQUEST_REJECTED') {
if (err) { console.error(`error confirming ${name}`, err);
console.error(err.stack); return dispatch(fail('confirm', err));
} }
dispatch(fail('confirm')); dispatch(fail('confirm'));
}); });
}; };

View File

@ -17,24 +17,37 @@
import { isAction, isStage } from '../util/actions'; import { isAction, isStage } from '../util/actions';
const initialState = { const initialState = {
error: null,
pending: false, pending: false,
queue: [] queue: []
}; };
export default (state = initialState, action) => { export default (state = initialState, action) => {
switch (action.type) {
case 'clearError':
return {
...state,
error: null
};
}
if (isAction('reverse', 'propose', action)) { if (isAction('reverse', 'propose', action)) {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...state, pending: true,
error: null,
queue: state.queue.concat({ queue: state.queue.concat({
action: 'propose', action: 'propose',
name: action.name, name: action.name,
address: action.address address: action.address
}) })
}; };
} else if (isStage('success', action) || isStage('fail', action)) { }
if (isStage('success', action) || isStage('fail', action)) {
return { return {
...state, pending: false, ...state, pending: false,
error: action.error || null,
queue: state.queue.filter((e) => queue: state.queue.filter((e) =>
e.action === 'propose' && e.action === 'propose' &&
e.name === action.name && e.name === action.name &&
@ -48,14 +61,18 @@ export default (state = initialState, action) => {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...state, pending: true,
error: null,
queue: state.queue.concat({ queue: state.queue.concat({
action: 'confirm', action: 'confirm',
name: action.name name: action.name
}) })
}; };
} else if (isStage('success', action) || isStage('fail', action)) { }
if (isStage('success', action) || isStage('fail', action)) {
return { return {
...state, pending: false, ...state, pending: false,
error: action.error || null,
queue: state.queue.filter((e) => queue: state.queue.filter((e) =>
e.action === 'confirm' && e.action === 'confirm' &&
e.name === action.name e.name === action.name

View File

@ -37,3 +37,6 @@
flex-shrink: 0; flex-shrink: 0;
} }
.error {
color: red;
}

View File

@ -21,17 +21,20 @@ import {
Card, CardHeader, CardText, TextField, DropDownMenu, MenuItem, RaisedButton Card, CardHeader, CardText, TextField, DropDownMenu, MenuItem, RaisedButton
} from 'material-ui'; } from 'material-ui';
import { nullableProptype } from '~/util/proptypes';
import { AddIcon, CheckIcon } from '~/ui/Icons'; import { AddIcon, CheckIcon } from '~/ui/Icons';
import { propose, confirm } from './actions'; import { clearError, confirm, propose } from './actions';
import styles from './reverse.css'; import styles from './reverse.css';
class Reverse extends Component { class Reverse extends Component {
static propTypes = { static propTypes = {
error: nullableProptype(PropTypes.object.isRequired),
pending: PropTypes.bool.isRequired, pending: PropTypes.bool.isRequired,
queue: PropTypes.array.isRequired, queue: PropTypes.array.isRequired,
propose: PropTypes.func.isRequired, clearError: PropTypes.func.isRequired,
confirm: PropTypes.func.isRequired confirm: PropTypes.func.isRequired,
propose: PropTypes.func.isRequired
} }
state = { state = {
@ -77,6 +80,7 @@ class Reverse extends Component {
</strong> </strong>
</p> </p>
{ explanation } { explanation }
{ this.renderError() }
<div className={ styles.box }> <div className={ styles.box }>
<DropDownMenu <DropDownMenu
disabled={ pending } disabled={ pending }
@ -108,6 +112,20 @@ class Reverse extends Component {
); );
} }
renderError () {
const { error } = this.props;
if (!error) {
return null;
}
return (
<div className={ styles.error }>
<code>{ error.message }</code>
</div>
);
}
onNameChange = (e) => { onNameChange = (e) => {
this.setState({ name: e.target.value }); this.setState({ name: e.target.value });
}; };
@ -129,9 +147,15 @@ class Reverse extends Component {
this.props.confirm(name); this.props.confirm(name);
} }
}; };
clearError = () => {
if (this.props.error) {
this.props.clearError();
}
};
} }
const mapStateToProps = (state) => state.reverse; 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); export default connect(mapStateToProps, mapDispatchToProps)(Reverse);

View File

@ -20,31 +20,48 @@ import { connect } from 'react-redux';
import Hash from './hash'; import Hash from './hash';
import etherscanUrl from '../util/etherscan-url'; import etherscanUrl from '../util/etherscan-url';
import IdentityIcon from '../IdentityIcon'; import IdentityIcon from '../IdentityIcon';
import { nullableProptype } from '~/util/proptypes';
import styles from './address.css'; import styles from './address.css';
class Address extends Component { class Address extends Component {
static propTypes = { static propTypes = {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
accounts: PropTypes.object.isRequired, account: nullableProptype(PropTypes.object.isRequired),
contacts: PropTypes.object.isRequired,
isTestnet: PropTypes.bool.isRequired, isTestnet: PropTypes.bool.isRequired,
key: PropTypes.string, key: PropTypes.string,
shortenHash: PropTypes.bool shortenHash: PropTypes.bool
} };
static defaultProps = { static defaultProps = {
key: 'address', key: 'address',
shortenHash: true shortenHash: true
} };
render () { render () {
const { address, accounts, contacts, isTestnet, key, shortenHash } = this.props; const { address, key } = this.props;
let caption; return (
if (accounts[address] || contacts[address]) { <div
const name = (accounts[address] || contacts[address] || {}).name; key={ key }
caption = ( className={ styles.container }
>
<IdentityIcon
address={ address }
className={ styles.align }
/>
{ this.renderCaption() }
</div>
);
}
renderCaption () {
const { address, account, isTestnet, shortenHash } = this.props;
if (account) {
const { name } = account;
return (
<a <a
className={ styles.link } className={ styles.link }
href={ etherscanUrl(address, isTestnet) } href={ etherscanUrl(address, isTestnet) }
@ -58,8 +75,9 @@ class Address extends Component {
</abbr> </abbr>
</a> </a>
); );
} else { }
caption = (
return (
<code className={ styles.align }> <code className={ styles.align }>
{ shortenHash ? ( { shortenHash ? (
<Hash <Hash
@ -70,29 +88,33 @@ class Address extends Component {
</code> </code>
); );
} }
}
return ( function mapStateToProps (initState, initProps) {
<div const { accounts, contacts } = initState;
key={ key }
className={ styles.container } const allAccounts = Object.assign({}, accounts.all, contacts);
>
<IdentityIcon // Add lower case addresses to map
address={ address } Object
className={ styles.align } .keys(allAccounts)
/> .forEach((address) => {
{ caption } allAccounts[address.toLowerCase()] = allAccounts[address];
</div> });
);
} return (state, props) => {
const { isTestnet } = state;
const { address = '' } = props;
const account = allAccounts[address] || null;
return {
account,
isTestnet
};
};
} }
export default connect( export default connect(
// mapStateToProps mapStateToProps
(state) => ({
accounts: state.accounts.all,
contacts: state.contacts,
isTestnet: state.isTestnet
}),
// mapDispatchToProps
null
)(Address); )(Address);

View File

@ -23,10 +23,20 @@ const styles = {
border: '1px solid #777' border: '1px solid #777'
}; };
export default (address) => ( export default (address) => {
if (!address || /^(0x)?0*$/.test(address)) {
return (
<code>
No image
</code>
);
}
return (
<img <img
src={ `${parityNode}/${address}/` } src={ `${parityNode}/${address}/` }
alt={ address } alt={ address }
style={ styles } style={ styles }
/> />
); );
};

View File

@ -19,7 +19,7 @@ export const isAction = (ns, type, action) => {
}; };
export const isStage = (stage, 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) => { export const addToQueue = (queue, action, name) => {
@ -27,5 +27,5 @@ export const addToQueue = (queue, action, name) => {
}; };
export const removeFromQueue = (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));
}; };

View File

@ -24,12 +24,6 @@ const postTx = (api, method, opt = {}, values = []) => {
}) })
.then((reqId) => { .then((reqId) => {
return api.pollMethod('parity_checkRequest', 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;
}); });
}; };

View File

@ -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 <http://www.gnu.org/licenses/>.
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);
};

View File

@ -20,6 +20,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import ReactTooltip from 'react-tooltip'; import ReactTooltip from 'react-tooltip';
import keycode from 'keycode';
import { Form, Input, IdentityIcon } from '~/ui'; import { Form, Input, IdentityIcon } from '~/ui';
@ -207,7 +208,9 @@ class TransactionPendingFormConfirm extends Component {
} }
onKeyDown = (event) => { onKeyDown = (event) => {
if (event.which !== 13) { const codeName = keycode(event);
if (codeName !== 'enter') {
return; return;
} }

View File

@ -71,7 +71,7 @@ class Wallet extends Component {
owned: PropTypes.bool.isRequired, owned: PropTypes.bool.isRequired,
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
wallet: PropTypes.object.isRequired, wallet: PropTypes.object.isRequired,
walletAccount: nullableProptype(PropTypes.object).isRequired walletAccount: nullableProptype(PropTypes.object.isRequired)
}; };
state = { state = {