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:
parent
4c532f9e3d
commit
63017268ad
@ -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",
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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 () {
|
||||
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,10 +46,17 @@ 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));
|
||||
|
||||
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
|
||||
@ -52,15 +65,15 @@ export const reserve = (name) => (dispatch, getState) => {
|
||||
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));
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
const values = [
|
||||
sha3(name)
|
||||
];
|
||||
|
||||
postTx(api, drop, options, values)
|
||||
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));
|
||||
});
|
||||
};
|
||||
|
@ -35,7 +35,12 @@
|
||||
.link {
|
||||
color: #00BCD4;
|
||||
text-decoration: none;
|
||||
}
|
||||
.link:hover {
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
@ -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));
|
||||
|
||||
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 setAddress = contract.functions.find((f) => f.name === fnName);
|
||||
|
||||
dispatch(start(name, key, value));
|
||||
const method = contract.instance[fnName];
|
||||
|
||||
const options = {
|
||||
from: account.address
|
||||
};
|
||||
|
||||
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());
|
||||
|
@ -36,3 +36,7 @@
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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));
|
||||
|
||||
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 = {
|
||||
from: account.address
|
||||
};
|
||||
|
||||
const values = [
|
||||
name,
|
||||
address
|
||||
];
|
||||
|
||||
postTx(api, proposeReverse, options, values)
|
||||
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));
|
||||
|
||||
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 = {
|
||||
from: account.address
|
||||
};
|
||||
|
||||
const values = [
|
||||
name
|
||||
];
|
||||
|
||||
postTx(api, confirmReverse, options, values)
|
||||
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'));
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -37,3 +37,6 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,8 +75,9 @@ class Address extends Component {
|
||||
</abbr>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
caption = (
|
||||
}
|
||||
|
||||
return (
|
||||
<code className={ styles.align }>
|
||||
{ shortenHash ? (
|
||||
<Hash
|
||||
@ -70,29 +88,33 @@ class Address extends Component {
|
||||
</code>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ key }
|
||||
className={ styles.container }
|
||||
>
|
||||
<IdentityIcon
|
||||
address={ address }
|
||||
className={ styles.align }
|
||||
/>
|
||||
{ caption }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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);
|
||||
|
@ -23,10 +23,20 @@ const styles = {
|
||||
border: '1px solid #777'
|
||||
};
|
||||
|
||||
export default (address) => (
|
||||
export default (address) => {
|
||||
if (!address || /^(0x)?0*$/.test(address)) {
|
||||
return (
|
||||
<code>
|
||||
No image
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={ `${parityNode}/${address}/` }
|
||||
alt={ address }
|
||||
style={ styles }
|
||||
/>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
37
js/src/dapps/registry/util/registry.js
Normal file
37
js/src/dapps/registry/util/registry.js
Normal 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);
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user