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",
"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",

View File

@ -14,8 +14,21 @@
// You should have received a copy of the GNU General Public License
// 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) {
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}`;
}

View File

@ -34,3 +34,16 @@ ReactDOM.render(
</Provider>,
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 = {
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 () {

View File

@ -15,11 +15,13 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
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(lookupAddress));
dispatch(reverseLookupStart(address));
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'));
});
};

View File

@ -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 = (
<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 (
<Card className={ styles.lookup }>
<CardHeader title={ 'Query the Registry' } />
@ -85,6 +60,7 @@ class Lookup extends Component {
hintText={ type === 'reverse' ? 'address' : 'name' }
value={ input }
onChange={ this.onInputChange }
onKeyDown={ this.onKeyDown }
/>
<DropDownMenu
value={ type }
@ -94,6 +70,7 @@ class Lookup extends Component {
<MenuItem value='IMG' primaryText='IMG  hash of a picture in the blockchain' />
<MenuItem value='CONTENT' primaryText='CONTENT  hash of a data in the blockchain' />
<MenuItem value='reverse' primaryText='reverse find a name for an address' />
<MenuItem value='owner' primaryText='owner find a the owner' />
</DropDownMenu>
<RaisedButton
label='Lookup'
@ -102,35 +79,102 @@ class Lookup extends Component {
onTouchTap={ this.onLookupClick }
/>
</div>
<CardText>{ output }</CardText>
<CardText>
{ this.renderOutput(type, result) }
</CardText>
</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) => {
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);

View File

@ -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;
}

View File

@ -15,8 +15,13 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
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`);
}
postTx(api, reserve, options, values)
const { reserve } = contract.instance;
const options = {
from: account.address,
value: fee
};
const values = [
sha3(name)
];
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}"`);
}
postTx(api, drop, options, values)
const { drop } = contract.instance;
const options = {
from: account.address
};
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));
});
};

View File

@ -35,7 +35,12 @@
.link {
color: #00BCD4;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.link:hover {
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 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 = (<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 {
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 {
: (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>)
)
}
{ this.renderError() }
<div className={ styles.box }>
<TextField
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) => {
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);

View File

@ -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)
};
}

View File

@ -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();
const fnName = key === 'A' ? 'setAddress' : 'set';
const setAddress = contract.functions.find((f) => f.name === fnName);
dispatch(start(name, key, value));
const options = {
from: account.address
};
const values = [
sha3(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}"`);
}
postTx(api, setAddress, options, values)
const fnName = key === 'A' ? 'setAddress' : 'set';
const method = contract.instance[fnName];
const options = {
from: account.address
};
const values = [
sha3(name),
key,
value
];
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());

View File

@ -36,3 +36,7 @@
flex-grow: 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 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 {
<p className={ styles.noSpacing }>
You can only modify entries of names that you previously registered.
</p>
{ this.renderError() }
<div className={ styles.box }>
<TextField
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) => {
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);

View File

@ -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
};
}

View File

@ -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();
const proposeReverse = contract.functions.find((f) => f.name === 'proposeReverse');
dispatch(start('propose', name, address));
const options = {
from: account.address
};
const values = [
name,
address
];
return getOwner(contract, name)
.then((owner) => {
if (owner.toLowerCase() !== account.address.toLowerCase()) {
throw new Error(`you are not the owner of "${name}"`);
}
postTx(api, proposeReverse, options, values)
const { proposeReverse } = contract.instance;
const options = {
from: account.address
};
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();
const confirmReverse = contract.functions.find((f) => f.name === 'confirmReverse');
dispatch(start('confirm', name));
const options = {
from: account.address
};
const values = [
name
];
return getOwner(contract, name)
.then((owner) => {
if (owner.toLowerCase() !== account.address.toLowerCase()) {
throw new Error(`you are not the owner of "${name}"`);
}
postTx(api, confirmReverse, options, values)
const { confirmReverse } = contract.instance;
const options = {
from: account.address
};
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'));
});
};

View File

@ -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

View File

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

View File

@ -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 {
</strong>
</p>
{ explanation }
{ this.renderError() }
<div className={ styles.box }>
<DropDownMenu
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) => {
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);

View File

@ -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;
let caption;
if (accounts[address] || contacts[address]) {
const name = (accounts[address] || contacts[address] || {}).name;
caption = (
return (
<div
key={ key }
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
className={ styles.link }
href={ etherscanUrl(address, isTestnet) }
@ -58,41 +75,46 @@ class Address extends Component {
</abbr>
</a>
);
} else {
caption = (
<code className={ styles.align }>
{ shortenHash ? (
<Hash
hash={ address }
linked
/>
) : address }
</code>
);
}
return (
<div
key={ key }
className={ styles.container }
>
<IdentityIcon
address={ address }
className={ styles.align }
/>
{ caption }
</div>
<code className={ styles.align }>
{ shortenHash ? (
<Hash
hash={ address }
linked
/>
) : address }
</code>
);
}
}
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);

View File

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

View File

@ -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));
};

View File

@ -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;
});
};

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 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;
}

View File

@ -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 = {