Merge master into jr-reverse-caching

This commit is contained in:
Jannis R
2017-01-06 13:35:50 +01:00
46 changed files with 2396 additions and 554 deletions

View File

@@ -218,14 +218,19 @@ export default class Contract {
}
_encodeOptions (func, options, values) {
options.data = this.getCallData(func, options, values);
return options;
const data = this.getCallData(func, options, values);
return {
...options,
data
};
}
_addOptionsTo (options = {}) {
return Object.assign({
to: this._address
}, options);
return {
to: this._address,
...options
};
}
_bindFunction = (func) => {

View File

@@ -31,6 +31,7 @@ const eth = new Api(transport);
describe('api/contract/Contract', () => {
const ADDR = '0x0123456789';
const ABI = [
{
type: 'function', name: 'test',
@@ -41,12 +42,42 @@ describe('api/contract/Contract', () => {
type: 'function', name: 'test2',
outputs: [{ type: 'uint' }, { type: 'uint' }]
},
{ type: 'constructor' },
{
type: 'constructor',
inputs: [{ name: 'boolin', type: 'bool' }, { name: 'stringin', type: 'string' }]
},
{ type: 'event', name: 'baz' },
{ type: 'event', name: 'foo' }
];
const VALUES = [true, 'jacogr'];
const ENCODED = '0x023562050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066a61636f67720000000000000000000000000000000000000000000000000000';
const ABI_NO_PARAMS = [
{
type: 'function', name: 'test',
inputs: [{ name: 'boolin', type: 'bool' }, { name: 'stringin', type: 'string' }],
outputs: [{ type: 'uint' }]
},
{
type: 'function', name: 'test2',
outputs: [{ type: 'uint' }, { type: 'uint' }]
},
{
type: 'constructor'
},
{ type: 'event', name: 'baz' },
{ type: 'event', name: 'foo' }
];
const VALUES = [ true, 'jacogr' ];
const CALLDATA = `
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000006
6a61636f67720000000000000000000000000000000000000000000000000000
`.replace(/\s/g, '');
const SIGNATURE = '02356205';
const ENCODED = `0x${SIGNATURE}${CALLDATA}`;
const RETURN1 = '0000000000000000000000000000000000000000000000000000000000123456';
const RETURN2 = '0000000000000000000000000000000000000000000000000000000000456789';
let scope;
@@ -230,6 +261,33 @@ describe('api/contract/Contract', () => {
});
});
describe('deploy without parameters', () => {
const contract = new Contract(eth, ABI_NO_PARAMS);
const CODE = '0x123';
const ADDRESS = '0xD337e80eEdBdf86eDBba021797d7e4e00Bb78351';
const RECEIPT_DONE = { contractAddress: ADDRESS.toLowerCase(), gasUsed: 50, blockNumber: 2500 };
let scope;
describe('success', () => {
before(() => {
scope = mockHttp([
{ method: 'eth_estimateGas', reply: { result: 1000 } },
{ method: 'parity_postTransaction', reply: { result: '0x678' } },
{ method: 'parity_checkRequest', reply: { result: '0x890' } },
{ method: 'eth_getTransactionReceipt', reply: { result: RECEIPT_DONE } },
{ method: 'eth_getCode', reply: { result: CODE } }
]);
return contract.deploy({ data: CODE }, []);
});
it('passes the options through to postTransaction (incl. gas calculation)', () => {
expect(scope.body.parity_postTransaction.params[0].data).to.equal(CODE);
});
});
});
describe('deploy', () => {
const contract = new Contract(eth, ABI);
const ADDRESS = '0xD337e80eEdBdf86eDBba021797d7e4e00Bb78351';
@@ -252,7 +310,7 @@ describe('api/contract/Contract', () => {
{ method: 'eth_getCode', reply: { result: '0x456' } }
]);
return contract.deploy({ data: '0x123' }, []);
return contract.deploy({ data: '0x123' }, VALUES);
});
it('calls estimateGas, postTransaction, checkRequest, getTransactionReceipt & getCode in order', () => {
@@ -261,7 +319,7 @@ describe('api/contract/Contract', () => {
it('passes the options through to postTransaction (incl. gas calculation)', () => {
expect(scope.body.parity_postTransaction.params).to.deep.equal([
{ data: '0x123', gas: '0x4b0' }
{ data: `0x123${CALLDATA}`, gas: '0x4b0' }
]);
});
@@ -280,7 +338,7 @@ describe('api/contract/Contract', () => {
]);
return contract
.deploy({ data: '0x123' }, [])
.deploy({ data: '0x123' }, VALUES)
.catch((error) => {
expect(error.message).to.match(/not deployed, gasUsed/);
});
@@ -296,7 +354,7 @@ describe('api/contract/Contract', () => {
]);
return contract
.deploy({ data: '0x123' }, [])
.deploy({ data: '0x123' }, VALUES)
.catch((error) => {
expect(error.message).to.match(/not deployed, getCode/);
});

View File

@@ -441,9 +441,7 @@ class DeployContract extends Component {
}
onCodeChange = (code) => {
const { api } = this.context;
this.setState(validateCode(code, api), this.estimateGas);
this.setState(validateCode(code), this.estimateGas);
}
onDeployStart = () => {

View File

@@ -14,25 +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 { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import Value from '../Value';
import styles from '../shapeshift.css';
@observer
export default class AwaitingDepositStep extends Component {
static propTypes = {
coinSymbol: PropTypes.string.isRequired,
depositAddress: PropTypes.string,
price: PropTypes.shape({
rate: PropTypes.number.isRequired,
minimum: PropTypes.number.isRequired,
limit: PropTypes.number.isRequired
}).isRequired
store: PropTypes.object.isRequired
}
render () {
const { coinSymbol, depositAddress, price } = this.props;
const { coinSymbol, depositAddress, price } = this.props.store;
const typeSymbol = (
<div className={ styles.symbol }>
{ coinSymbol }
@@ -43,22 +39,38 @@ export default class AwaitingDepositStep extends Component {
return (
<div className={ styles.center }>
<div className={ styles.busy }>
Awaiting confirmation of the deposit address for your { typeSymbol } funds exchange
<FormattedMessage
id='shapeshift.awaitingDepositStep.awaitingConfirmation'
defaultMessage='Awaiting confirmation of the deposit address for your {typeSymbol} funds exchange'
values={ { typeSymbol } } />
</div>
</div>
);
}
return (
<div className={ styles.center }>
<div className={ styles.info }>
<a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> is awaiting a { typeSymbol } deposit. Send the funds from your { typeSymbol } network client to -
<FormattedMessage
id='shapeshift.awaitingDepositStep.awaitingDeposit'
defaultMessage='{shapeshiftLink} is awaiting a {typeSymbol} deposit. Send the funds from your {typeSymbol} network client to -'
values={ {
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>,
typeSymbol
} } />
</div>
<div className={ styles.hero }>
{ depositAddress }
</div>
<div className={ styles.price }>
<div>
(<Value amount={ price.minimum } symbol={ coinSymbol } /> minimum, <Value amount={ price.limit } symbol={ coinSymbol } /> maximum)
<FormattedMessage
id='shapeshift.awaitingDepositStep.minimumMaximum'
defaultMessage='{minimum} minimum, {maximum} maximum'
values={ {
maximum: <Value amount={ price.limit } symbol={ coinSymbol } />,
minimum: <Value amount={ price.minimum } symbol={ coinSymbol } />
} } />
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
// 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/>.
import { shallow } from 'enzyme';
import React from 'react';
import AwaitingDepositStep from './';
let component;
function render () {
component = shallow(
<AwaitingDepositStep
store={ {
coinSymbol: 'BTC',
price: { rate: 0.001, minimum: 0, limit: 1.999 }
} } />
);
return component;
}
describe('modals/Shapeshift/AwaitingDepositStep', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
it('displays waiting for address with empty depositAddress', () => {
render();
expect(component.find('FormattedMessage').props().id).to.match(/awaitingConfirmation/);
});
it('displays waiting for deposit with non-empty depositAddress', () => {
render({ depositAddress: 'xyz' });
expect(component.find('FormattedMessage').first().props().id).to.match(/awaitingDeposit/);
});
});

View File

@@ -15,32 +15,39 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import Value from '../Value';
import { FormattedMessage } from 'react-intl';
import { observer } from 'mobx-react';
import Value from '../Value';
import styles from '../shapeshift.css';
@observer
export default class AwaitingExchangeStep extends Component {
static propTypes = {
depositInfo: PropTypes.shape({
incomingCoin: PropTypes.number.isRequired,
incomingType: PropTypes.string.isRequired
}).isRequired
store: PropTypes.object.isRequired
}
render () {
const { depositInfo } = this.props;
const { depositInfo } = this.props.store;
const { incomingCoin, incomingType } = depositInfo;
return (
<div className={ styles.center }>
<div className={ styles.info }>
<a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> has received a deposit of -
<FormattedMessage
id='shapeshift.awaitingExchangeStep.receivedInfo'
defaultMessage='{shapeshiftLink} has received a deposit of -'
values={ {
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>
} } />
</div>
<div className={ styles.hero }>
<Value amount={ incomingCoin } symbol={ incomingType } />
</div>
<div className={ styles.info }>
Awaiting the completion of the funds exchange and transfer of funds to your Parity account.
<FormattedMessage
id='shapeshift.awaitingExchangeStep.awaitingCompletion'
defaultMessage='Awaiting the completion of the funds exchange and transfer of funds to your Parity account.' />
</div>
</div>
);

View File

@@ -0,0 +1,39 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import AwaitingExchangeStep from './';
let component;
function render () {
component = shallow(
<AwaitingExchangeStep
store={ {
depositInfo: { incomingCoin: 0.01, incomingType: 'BTC' }
} } />
);
return component;
}
describe('modals/Shapeshift/AwaitingExchangeStep', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@@ -14,39 +14,41 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import Value from '../Value';
import styles from '../shapeshift.css';
@observer
export default class CompletedStep extends Component {
static propTypes = {
depositInfo: PropTypes.shape({
incomingCoin: PropTypes.number.isRequired,
incomingType: PropTypes.string.isRequired
}).isRequired,
exchangeInfo: PropTypes.shape({
outgoingCoin: PropTypes.string.isRequired,
outgoingType: PropTypes.string.isRequired
}).isRequired
store: PropTypes.object.isRequired
}
render () {
const { depositInfo, exchangeInfo } = this.props;
const { depositInfo, exchangeInfo } = this.props.store;
const { incomingCoin, incomingType } = depositInfo;
const { outgoingCoin, outgoingType } = exchangeInfo;
return (
<div className={ styles.center }>
<div className={ styles.info }>
<a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> has completed the funds exchange.
<FormattedMessage
id='shapeshift.completedStep.completed'
defaultMessage='{shapeshiftLink} has completed the funds exchange.'
values={ {
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>
} } />
</div>
<div className={ styles.hero }>
<Value amount={ incomingCoin } symbol={ incomingType } /> => <Value amount={ outgoingCoin } symbol={ outgoingType } />
</div>
<div className={ styles.info }>
The change in funds will be reflected in your Parity account shortly.
<FormattedMessage
id='shapeshift.completedStep.parityFunds'
defaultMessage='The change in funds will be reflected in your Parity account shortly.' />
</div>
</div>
);

View File

@@ -0,0 +1,40 @@
// 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/>.
import { shallow } from 'enzyme';
import React from 'react';
import CompletedStep from './';
let component;
function render () {
component = shallow(
<CompletedStep
store={ {
depositInfo: { incomingCoin: 0.01, incomingType: 'BTC' },
exchangeInfo: { outgoingCoin: 0.1, outgoingType: 'ETH' }
} } />
);
return component;
}
describe('modals/Shapeshift/CompletedStep', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@@ -14,25 +14,30 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import styles from '../shapeshift.css';
@observer
export default class ErrorStep extends Component {
static propTypes = {
error: PropTypes.shape({
fatal: PropTypes.bool,
message: PropTypes.string.isRequired
}).isRequired
store: PropTypes.object.isRequired
}
render () {
const { error } = this.props;
const { error } = this.props.store;
return (
<div className={ styles.body }>
<div className={ styles.info }>
The funds shifting via <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> failed with a fatal error on the exchange. The error message received from the exchange is as follow:
<FormattedMessage
id='shapeshift.errorStep.info'
defaultMessage='The funds shifting via {shapeshiftLink} failed with a fatal error on the exchange. The error message received from the exchange is as follow:'
values={ {
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>
} } />
</div>
<div className={ styles.error }>
{ error.message }

View File

@@ -0,0 +1,39 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import ErrorStep from './';
let component;
function render () {
component = shallow(
<ErrorStep
store={ {
error: new Error('testing')
} } />
);
return component;
}
describe('modals/Shapeshift/ErrorStep', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@@ -14,64 +14,93 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { Checkbox, MenuItem } from 'material-ui';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Form, Input, Select } from '~/ui';
import { Form, Input, Select, Warning } from '~/ui';
import Price from '../Price';
import { WARNING_NO_PRICE } from '../store';
import styles from './optionsStep.css';
const WARNING_LABELS = {
[WARNING_NO_PRICE]: (
<FormattedMessage
id='shapeshift.warning.noPrice'
defaultMessage='No price match was found for the selected type' />
)
};
@observer
export default class OptionsStep extends Component {
static propTypes = {
refundAddress: PropTypes.string.isRequired,
coinSymbol: PropTypes.string.isRequired,
coins: PropTypes.array.isRequired,
price: PropTypes.object,
hasAccepted: PropTypes.bool.isRequired,
onChangeSymbol: PropTypes.func.isRequired,
onChangeRefund: PropTypes.func.isRequired,
onToggleAccept: PropTypes.func.isRequired
store: PropTypes.object.isRequired
};
render () {
const { coinSymbol, coins, refundAddress, hasAccepted, onToggleAccept } = this.props;
const label = `(optional) ${coinSymbol} return address`;
const { coinSymbol, coins, hasAcceptedTerms, price, refundAddress, warning } = this.props.store;
if (!coins.length) {
return (
<div className={ styles.empty }>
There are currently no exchange pairs/coins available to fund with.
<FormattedMessage
id='shapeshift.optionsStep.noPairs'
defaultMessage='There are currently no exchange pairs/coins available to fund with.' />
</div>
);
}
const items = coins.map(this.renderCoinSelectItem);
return (
<div className={ styles.body }>
<Form>
<Select
className={ styles.coinselector }
label='fund account from'
hint='the type of crypto conversion to do'
value={ coinSymbol }
onChange={ this.onSelectCoin }>
{ items }
hint={
<FormattedMessage
id='shapeshift.optionsStep.typeSelect.hint'
defaultMessage='the type of crypto conversion to do' />
}
label={
<FormattedMessage
id='shapeshift.optionsStep.typeSelect.label'
defaultMessage='fund account from' />
}
onChange={ this.onSelectCoin }
value={ coinSymbol }>
{
coins.map(this.renderCoinSelectItem)
}
</Select>
<Input
label={ label }
hint='the return address for send failures'
value={ refundAddress }
onSubmit={ this.onChangeRefund } />
hint={
<FormattedMessage
id='shapeshift.optionsStep.returnAddr.hint'
defaultMessage='the return address for send failures' />
}
label={
<FormattedMessage
id='shapeshift.optionsStep.returnAddr.label'
defaultMessage='(optional) {coinSymbol} return address'
values={ { coinSymbol } } />
}
onSubmit={ this.onChangeRefundAddress }
value={ refundAddress } />
<Checkbox
checked={ hasAcceptedTerms }
className={ styles.accept }
label='I understand that ShapeShift.io is a 3rd-party service and by using the service any transfer of information and/or funds is completely out of the control of Parity'
checked={ hasAccepted }
onCheck={ onToggleAccept } />
label={
<FormattedMessage
id='shapeshift.optionsStep.terms.label'
defaultMessage='I understand that ShapeShift.io is a 3rd-party service and by using the service any transfer of information and/or funds is completely out of the control of Parity' />
}
onCheck={ this.onToggleAcceptTerms } />
</Form>
<Price { ...this.props } />
<Warning warning={ WARNING_LABELS[warning] } />
<Price
coinSymbol={ coinSymbol }
price={ price } />
</div>
);
}
@@ -81,7 +110,9 @@ export default class OptionsStep extends Component {
const item = (
<div className={ styles.coinselect }>
<img className={ styles.coinimage } src={ image } />
<img
className={ styles.coinimage }
src={ image } />
<div className={ styles.coindetails }>
<div className={ styles.coinsymbol }>
{ symbol }
@@ -103,11 +134,15 @@ export default class OptionsStep extends Component {
);
}
onSelectCoin = (event, idx, value) => {
this.props.onChangeSymbol(event, value);
onChangeRefundAddress = (event, refundAddress) => {
this.props.store.setRefundAddress(refundAddress);
}
onChangeAddress = (event, value) => {
this.props.onChangeRefund(value);
onSelectCoin = (event, index, coinSymbol) => {
this.props.store.setCoinSymbol(coinSymbol);
}
onToggleAcceptTerms = () => {
this.props.store.toggleAcceptTerms();
}
}

View File

@@ -0,0 +1,126 @@
// 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/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import Store, { WARNING_NO_PRICE } from '../store';
import OptionsStep from './';
const ADDRESS = '0x1234567890123456789012345678901234567890';
let component;
let instance;
let store;
function render () {
store = new Store(ADDRESS);
component = shallow(
<OptionsStep store={ store } />
);
instance = component.instance();
return component;
}
describe('modals/Shapeshift/OptionsStep', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders no coins when none available', () => {
expect(component.find('FormattedMessage').props().id).to.equal('shapeshift.optionsStep.noPairs');
});
describe('components', () => {
beforeEach(() => {
store.setCoins([{ symbol: 'BTC', name: 'Bitcoin' }]);
store.toggleAcceptTerms();
});
describe('terms Checkbox', () => {
it('shows the state of store.hasAcceptedTerms', () => {
expect(component.find('Checkbox').props().checked).to.be.true;
});
});
describe('warning', () => {
let warning;
beforeEach(() => {
store.setWarning(WARNING_NO_PRICE);
warning = component.find('Warning');
});
it('shows a warning message when available', () => {
expect(warning.props().warning.props.id).to.equal('shapeshift.warning.noPrice');
});
});
});
describe('events', () => {
describe('onChangeRefundAddress', () => {
beforeEach(() => {
sinon.stub(store, 'setRefundAddress');
});
afterEach(() => {
store.setRefundAddress.restore();
});
it('sets the refundAddress on the store', () => {
instance.onChangeRefundAddress(null, 'refundAddress');
expect(store.setRefundAddress).to.have.been.calledWith('refundAddress');
});
});
describe('onSelectCoin', () => {
beforeEach(() => {
sinon.stub(store, 'setCoinSymbol');
});
afterEach(() => {
store.setCoinSymbol.restore();
});
it('sets the coinSymbol on the store', () => {
instance.onSelectCoin(null, 0, 'XMR');
expect(store.setCoinSymbol).to.have.been.calledWith('XMR');
});
});
describe('onToggleAcceptTerms', () => {
beforeEach(() => {
sinon.stub(store, 'toggleAcceptTerms');
});
afterEach(() => {
store.toggleAcceptTerms.restore();
});
it('toggles the terms on the store', () => {
instance.onToggleAcceptTerms();
expect(store.toggleAcceptTerms).to.have.been.called;
});
});
});
});

View File

@@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import Value from '../Value';
import styles from '../shapeshift.css';
@@ -42,7 +43,13 @@ export default class Price extends Component {
<Value amount={ 1 } symbol={ coinSymbol } /> = <Value amount={ price.rate } />
</div>
<div>
(<Value amount={ price.minimum } symbol={ coinSymbol } /> minimum, <Value amount={ price.limit } symbol={ coinSymbol } /> maximum)
<FormattedMessage
id='shapeshift.price.minMax'
defaultMessage='({minimum} minimum, {maximum} maximum)'
values={ {
maximum: <Value amount={ price.limit } symbol={ coinSymbol } />,
minimum: <Value amount={ price.minimum } symbol={ coinSymbol } />
} } />
</div>
</div>
);

View File

@@ -0,0 +1,40 @@
// 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/>.
import { shallow } from 'enzyme';
import React from 'react';
import Price from './';
let component;
function render (props = {}) {
component = shallow(
<Price
coinSymbol='BTC'
price={ { rate: 0.1, minimum: 0.1, limit: 0.9 } }
error={ new Error('testing') }
{ ...props } />
);
return component;
}
describe('modals/Shapeshift/Price', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@@ -0,0 +1,36 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import Value from './';
let component;
function render (props = {}) {
component = shallow(
<Value { ...props } />
);
return component;
}
describe('modals/Shapeshift/Value', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@@ -14,26 +14,44 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
import { FormattedMessage } from 'react-intl';
import shapeshiftLogo from '~/../assets/images/shapeshift-logo.png';
import { Button, IdentityIcon, Modal } from '~/ui';
import initShapeshift from '~/3rdparty/shapeshift';
import shapeshiftLogo from '../../../assets/images/shapeshift-logo.png';
import { CancelIcon, DoneIcon } from '~/ui/Icons';
import AwaitingDepositStep from './AwaitingDepositStep';
import AwaitingExchangeStep from './AwaitingExchangeStep';
import CompletedStep from './CompletedStep';
import ErrorStep from './ErrorStep';
import OptionsStep from './OptionsStep';
import Store, { STAGE_COMPLETED, STAGE_OPTIONS, STAGE_WAIT_DEPOSIT, STAGE_WAIT_EXCHANGE } from './store';
import styles from './shapeshift.css';
const shapeshift = initShapeshift();
const STAGE_NAMES = ['details', 'awaiting deposit', 'awaiting exchange', 'completed'];
const STAGE_TITLES = [
<FormattedMessage
id='shapeshift.title.details'
defaultMessage='details' />,
<FormattedMessage
id='shapeshift.title.deposit'
defaultMessage='awaiting deposit' />,
<FormattedMessage
id='shapeshift.title.exchange'
defaultMessage='awaiting exchange' />,
<FormattedMessage
id='shapeshift.title.completed'
defaultMessage='completed' />
];
const ERROR_TITLE = (
<FormattedMessage
id='shapeshift.title.error'
defaultMessage='exchange failed' />
);
@observer
export default class Shapeshift extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
@@ -44,46 +62,38 @@ export default class Shapeshift extends Component {
onClose: PropTypes.func
}
state = {
stage: 0,
coinSymbol: 'BTC',
coinPair: 'btc_eth',
coins: [],
depositAddress: '',
refundAddress: '',
price: null,
depositInfo: null,
exchangeInfo: null,
error: {},
hasAccepted: false,
shifting: false
}
store = new Store(this.props.address);
componentDidMount () {
this.retrieveCoins();
this.store.retrieveCoins();
}
componentWillUnmount () {
this.unsubscribe();
}
unsubscribe () {
// Unsubscribe from Shapeshit
const { depositAddress } = this.state;
shapeshift.unsubscribe(depositAddress);
this.store.unsubscribe();
}
render () {
const { error, stage } = this.state;
const { error, stage } = this.store;
return (
<Modal
actions={ this.renderDialogActions() }
current={ stage }
steps={ error.fatal ? null : STAGE_NAMES }
title={ error.fatal ? 'exchange failed' : null }
waiting={ [1, 2] }
visible>
steps={
error
? null
: STAGE_TITLES
}
title={
error
? ERROR_TITLE
: null
}
visible
waiting={ [
STAGE_WAIT_DEPOSIT,
STAGE_WAIT_EXCHANGE
] }>
{ this.renderPage() }
</Modal>
);
@@ -91,7 +101,7 @@ export default class Shapeshift extends Component {
renderDialogActions () {
const { address } = this.props;
const { coins, error, stage, hasAccepted, shifting } = this.state;
const { coins, error, hasAcceptedTerms, stage } = this.store;
const logo = (
<a href='http://shapeshift.io' target='_blank' className={ styles.shapeshift }>
@@ -100,12 +110,16 @@ export default class Shapeshift extends Component {
);
const cancelBtn = (
<Button
icon={ <ContentClear /> }
label='Cancel'
icon={ <CancelIcon /> }
label={
<FormattedMessage
id='shapeshift.button.cancel'
defaultMessage='Cancel' />
}
onClick={ this.onClose } />
);
if (error.fatal) {
if (error) {
return [
logo,
cancelBtn
@@ -113,208 +127,85 @@ export default class Shapeshift extends Component {
}
switch (stage) {
case 0:
case STAGE_OPTIONS:
return [
logo,
cancelBtn,
<Button
disabled={ !coins.length || !hasAccepted || shifting }
icon={ <IdentityIcon address={ address } button /> }
label='Shift Funds'
disabled={ !coins.length || !hasAcceptedTerms }
icon={
<IdentityIcon
address={ address }
button />
}
label={
<FormattedMessage
id='shapeshift.button.shift'
defaultMessage='Shift Funds' />
}
onClick={ this.onShift } />
];
case 1:
case 2:
case STAGE_WAIT_DEPOSIT:
case STAGE_WAIT_EXCHANGE:
return [
logo,
cancelBtn
];
case 3:
case STAGE_COMPLETED:
return [
logo,
<Button
icon={ <ActionDoneAll /> }
label='Close'
icon={ <DoneIcon /> }
label={
<FormattedMessage
id='shapeshift.button.done'
defaultMessage='Close' />
}
onClick={ this.onClose } />
];
}
}
renderPage () {
const { error, stage } = this.state;
const { error, stage } = this.store;
if (error.fatal) {
if (error) {
return (
<ErrorStep error={ error } />
<ErrorStep store={ this.store } />
);
}
switch (stage) {
case 0:
case STAGE_OPTIONS:
return (
<OptionsStep
{ ...this.state }
onChangeSymbol={ this.onChangeSymbol }
onChangeRefund={ this.onChangeRefund }
onToggleAccept={ this.onToggleAccept } />
<OptionsStep store={ this.store } />
);
case 1:
case STAGE_WAIT_DEPOSIT:
return (
<AwaitingDepositStep { ...this.state } />
<AwaitingDepositStep store={ this.store } />
);
case 2:
case STAGE_WAIT_EXCHANGE:
return (
<AwaitingExchangeStep { ...this.state } />
<AwaitingExchangeStep store={ this.store } />
);
case 3:
case STAGE_COMPLETED:
return (
<CompletedStep { ...this.state } />
<CompletedStep store={ this.store } />
);
}
}
setStage (stage) {
this.setState({
stage,
error: {}
});
}
setFatalError (message) {
this.setState({
stage: 0,
error: {
fatal: true,
message
}
});
}
onClose = () => {
this.setStage(0);
this.store.setStage(STAGE_OPTIONS);
this.props.onClose && this.props.onClose();
}
onShift = () => {
const { address } = this.props;
const { coinPair, refundAddress } = this.state;
this.setState({
stage: 1,
shifting: true
});
shapeshift
.shift(address, refundAddress, coinPair)
.then((result) => {
console.log('onShift', result);
const depositAddress = result.deposit;
if (this.state.depositAddress) {
this.unsubscribe();
}
shapeshift.subscribe(depositAddress, this.onExchangeInfo);
this.setState({ depositAddress });
})
.catch((error) => {
console.error('onShift', error);
const message = `Failed to start exchange: ${error.message}`;
this.newError(new Error(message));
this.setFatalError(message);
});
}
onChangeSymbol = (event, coinSymbol) => {
const coinPair = `${coinSymbol.toLowerCase()}_eth`;
this.setState({
coinPair,
coinSymbol,
price: null
});
this.getPrice(coinPair);
}
onChangeRefund = (event, refundAddress) => {
this.setState({ refundAddress });
}
onToggleAccept = () => {
const { hasAccepted } = this.state;
this.setState({
hasAccepted: !hasAccepted
});
}
onExchangeInfo = (error, result) => {
if (error) {
console.error('onExchangeInfo', error);
if (error.fatal) {
this.setFatalError(error.message);
}
this.newError(error);
return;
}
console.log('onExchangeInfo', result.status, result);
switch (result.status) {
case 'received':
this.setState({ depositInfo: result });
this.setStage(2);
return;
case 'complete':
this.setState({ exchangeInfo: result });
this.setStage(3);
return;
}
}
getPrice (coinPair) {
shapeshift
.getMarketInfo(coinPair)
.then((price) => {
this.setState({ price });
})
.catch((error) => {
console.error('getPrice', error);
});
}
retrieveCoins () {
const { coinPair } = this.state;
shapeshift
.getCoins()
.then((_coins) => {
const coins = Object.values(_coins).filter((coin) => coin.status === 'available');
this.getPrice(coinPair);
this.setState({ coins });
})
.catch((error) => {
console.error('retrieveCoins', error);
const message = `Failed to retrieve available coins from ShapeShift.io: ${error.message}`;
this.newError(new Error(message));
this.setFatalError(message);
});
}
newError (error) {
const { store } = this.context;
store.dispatch({ type: 'newError', error });
return this.store.shift();
}
}

View File

@@ -0,0 +1,159 @@
// 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/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import { STAGE_COMPLETED, STAGE_OPTIONS, STAGE_WAIT_DEPOSIT, STAGE_WAIT_EXCHANGE } from './store';
import Shapeshift from './';
const ADDRESS = '0x0123456789012345678901234567890123456789';
let component;
let instance;
let onClose;
function render (props = {}) {
onClose = sinon.stub();
component = shallow(
<Shapeshift
address={ ADDRESS }
onClose={ onClose }
{ ...props } />,
{ context: { store: {} } }
);
instance = component.instance();
return component;
}
describe('modals/Shapeshift', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('componentDidMount', () => {
beforeEach(() => {
render();
sinon.stub(instance.store, 'retrieveCoins');
return instance.componentDidMount();
});
afterEach(() => {
instance.store.retrieveCoins.restore();
});
it('retrieves the list of coins when mounting', () => {
expect(instance.store.retrieveCoins).to.have.been.called;
});
});
describe('componentWillUnmount', () => {
beforeEach(() => {
render();
sinon.stub(instance.store, 'unsubscribe');
return instance.componentWillUnmount();
});
afterEach(() => {
instance.store.unsubscribe.restore();
});
it('removes any subscriptions when unmounting', () => {
expect(instance.store.unsubscribe).to.have.been.called;
});
});
describe('renderDialogActions', () => {
beforeEach(() => {
render();
});
describe('shift button', () => {
beforeEach(() => {
sinon.stub(instance.store, 'shift').resolves();
instance.store.setCoins(['BTC']);
instance.store.toggleAcceptTerms();
});
afterEach(() => {
instance.store.shift.restore();
});
it('disabled shift button when not accepted', () => {
instance.store.toggleAcceptTerms();
expect(shallow(instance.renderDialogActions()[2]).props().disabled).to.be.true;
});
it('shows shift button when accepted', () => {
expect(shallow(instance.renderDialogActions()[2]).props().disabled).to.be.false;
});
it('calls the shift on store when clicked', () => {
shallow(instance.renderDialogActions()[2]).simulate('touchTap');
expect(instance.store.shift).to.have.been.called;
});
});
});
describe('renderPage', () => {
beforeEach(() => {
render();
});
it('renders ErrorStep on error, passing the store', () => {
instance.store.setError('testError');
const page = instance.renderPage();
expect(page.type).to.match(/ErrorStep/);
expect(page.props.store).to.equal(instance.store);
});
it('renders OptionsStep with STAGE_OPTIONS, passing the store', () => {
instance.store.setStage(STAGE_OPTIONS);
const page = instance.renderPage();
expect(page.type).to.match(/OptionsStep/);
expect(page.props.store).to.equal(instance.store);
});
it('renders AwaitingDepositStep with STAGE_WAIT_DEPOSIT, passing the store', () => {
instance.store.setStage(STAGE_WAIT_DEPOSIT);
const page = instance.renderPage();
expect(page.type).to.match(/AwaitingDepositStep/);
expect(page.props.store).to.equal(instance.store);
});
it('renders AwaitingExchangeStep with STAGE_WAIT_EXCHANGE, passing the store', () => {
instance.store.setStage(STAGE_WAIT_EXCHANGE);
const page = instance.renderPage();
expect(page.type).to.match(/AwaitingExchangeStep/);
expect(page.props.store).to.equal(instance.store);
});
it('renders CompletedStep with STAGE_COMPLETED, passing the store', () => {
instance.store.setStage(STAGE_COMPLETED);
const page = instance.renderPage();
expect(page.type).to.match(/CompletedStep/);
expect(page.props.store).to.equal(instance.store);
});
});
});

View File

@@ -0,0 +1,199 @@
// 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/>.
import { action, observable, transaction } from 'mobx';
import initShapeshift from '~/3rdparty/shapeshift';
const STAGE_OPTIONS = 0;
const STAGE_WAIT_DEPOSIT = 1;
const STAGE_WAIT_EXCHANGE = 2;
const STAGE_COMPLETED = 3;
const WARNING_NONE = 0;
const WARNING_NO_PRICE = -1;
export default class Store {
@observable address = null;
@observable coinPair = 'btc_eth';
@observable coinSymbol = 'BTC';
@observable coins = [];
@observable depositAddress = '';
@observable depositInfo = null;
@observable exchangeInfo = null;
@observable error = null;
@observable hasAcceptedTerms = false;
@observable price = null;
@observable refundAddress = '';
@observable stage = STAGE_OPTIONS;
@observable warning = 0;
constructor (address) {
this._shapeshiftApi = initShapeshift();
this.address = address;
}
@action setCoins = (coins) => {
this.coins = coins;
}
@action setCoinSymbol = (coinSymbol) => {
transaction(() => {
this.coinSymbol = coinSymbol;
this.coinPair = `${coinSymbol.toLowerCase()}_eth`;
this.price = null;
});
return this.getCoinPrice();
}
@action setDepositAddress = (depositAddress) => {
this.depositAddress = depositAddress;
}
@action setDepositInfo = (depositInfo) => {
transaction(() => {
this.depositInfo = depositInfo;
this.setStage(STAGE_WAIT_EXCHANGE);
});
}
@action setError = (error) => {
this.error = error;
}
@action setExchangeInfo = (exchangeInfo) => {
transaction(() => {
this.exchangeInfo = exchangeInfo;
this.setStage(STAGE_COMPLETED);
});
}
@action setPrice = (price) => {
transaction(() => {
this.price = price;
this.setWarning();
});
}
@action setRefundAddress = (refundAddress) => {
this.refundAddress = refundAddress;
}
@action setStage = (stage) => {
this.stage = stage;
}
@action setWarning = (warning = WARNING_NONE) => {
this.warning = warning;
}
@action toggleAcceptTerms = () => {
this.hasAcceptedTerms = !this.hasAcceptedTerms;
}
getCoinPrice () {
return this._shapeshiftApi
.getMarketInfo(this.coinPair)
.then((price) => {
this.setPrice(price);
})
.catch((error) => {
console.warn('getCoinPrice', error);
this.setWarning(WARNING_NO_PRICE);
});
}
retrieveCoins () {
return this._shapeshiftApi
.getCoins()
.then((coins) => {
this.setCoins(Object.values(coins).filter((coin) => coin.status === 'available'));
return this.getCoinPrice();
})
.catch((error) => {
console.error('retrieveCoins', error);
const message = `Failed to retrieve available coins from ShapeShift.io: ${error.message}`;
this.setError(message);
});
}
shift () {
this.setStage(STAGE_WAIT_DEPOSIT);
return this._shapeshiftApi
.shift(this.address, this.refundAddress, this.coinPair)
.then((result) => {
console.log('onShift', result);
this.setDepositAddress(result.deposit);
return this.subscribe();
})
.catch((error) => {
console.error('onShift', error);
const message = `Failed to start exchange: ${error.message}`;
this.setError(new Error(message));
});
}
onExchangeInfo = (error, result) => {
if (error) {
console.error('onExchangeInfo', error);
if (error.fatal) {
this.setError(error);
}
return;
}
console.log('onExchangeInfo', result.status, result);
switch (result.status) {
case 'received':
if (this.stage !== STAGE_WAIT_EXCHANGE) {
this.setDepositInfo(result);
}
return;
case 'complete':
if (this.stage !== STAGE_COMPLETED) {
this.setExchangeInfo(result);
}
return;
}
}
subscribe () {
return this._shapeshiftApi.subscribe(this.depositAddress, this.onExchangeInfo);
}
unsubscribe () {
return this._shapeshiftApi.unsubscribe(this.depositAddress);
}
}
export {
STAGE_COMPLETED,
STAGE_OPTIONS,
STAGE_WAIT_DEPOSIT,
STAGE_WAIT_EXCHANGE,
WARNING_NONE,
WARNING_NO_PRICE
};

View File

@@ -0,0 +1,355 @@
// 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/>.
import sinon from 'sinon';
import Store, { STAGE_COMPLETED, STAGE_OPTIONS, STAGE_WAIT_DEPOSIT, STAGE_WAIT_EXCHANGE, WARNING_NONE, WARNING_NO_PRICE } from './store';
const ADDRESS = '0xabcdeffdecbaabcdeffdecbaabcdeffdecbaabcdeffdecba';
describe('modals/Shapeshift/Store', () => {
let store;
beforeEach(() => {
store = new Store(ADDRESS);
});
it('stores the ETH address', () => {
expect(store.address).to.equal(ADDRESS);
});
it('defaults to BTC-ETH pair', () => {
expect(store.coinSymbol).to.equal('BTC');
expect(store.coinPair).to.equal('btc_eth');
});
it('defaults to stage STAGE_OPTIONS', () => {
expect(store.stage).to.equal(STAGE_OPTIONS);
});
it('defaults to terms not accepted', () => {
expect(store.hasAcceptedTerms).to.be.false;
});
describe('@action', () => {
describe('setCoins', () => {
it('sets the available coins', () => {
const coins = ['BTC', 'ETC', 'XMR'];
store.setCoins(coins);
expect(store.coins.peek()).to.deep.equal(coins);
});
});
describe('setCoinSymbol', () => {
beforeEach(() => {
sinon.stub(store, 'getCoinPrice');
store.setCoinSymbol('XMR');
});
afterEach(() => {
store.getCoinPrice.restore();
});
it('sets the coinSymbol', () => {
expect(store.coinSymbol).to.equal('XMR');
});
it('sets the coinPair', () => {
expect(store.coinPair).to.equal('xmr_eth');
});
it('resets the price retrieved', () => {
expect(store.price).to.be.null;
});
it('retrieves the pair price', () => {
expect(store.getCoinPrice).to.have.been.called;
});
});
describe('setDepositAddress', () => {
it('sets the depositAddress', () => {
store.setDepositAddress('testing');
expect(store.depositAddress).to.equal('testing');
});
});
describe('setDepositInfo', () => {
beforeEach(() => {
store.setDepositInfo('testing');
});
it('sets the depositInfo', () => {
expect(store.depositInfo).to.equal('testing');
});
it('sets the stage to STAGE_WAIT_EXCHANGE', () => {
expect(store.stage).to.equal(STAGE_WAIT_EXCHANGE);
});
});
describe('setError', () => {
it('sets the error', () => {
store.setError(new Error('testing'));
expect(store.error).to.match(/testing/);
});
});
describe('setExchangeInfo', () => {
beforeEach(() => {
store.setExchangeInfo('testing');
});
it('sets the exchangeInfo', () => {
expect(store.exchangeInfo).to.equal('testing');
});
it('sets the stage to STAGE_COMPLETED', () => {
expect(store.stage).to.equal(STAGE_COMPLETED);
});
});
describe('setPrice', () => {
it('sets the price', () => {
store.setPrice('testing');
expect(store.price).to.equal('testing');
});
it('clears any warnings once set', () => {
store.setWarning(-999);
store.setPrice('testing');
expect(store.warning).to.equal(WARNING_NONE);
});
});
describe('setRefundAddress', () => {
it('sets the price', () => {
store.setRefundAddress('testing');
expect(store.refundAddress).to.equal('testing');
});
});
describe('setStage', () => {
it('sets the state', () => {
store.setStage('testing');
expect(store.stage).to.equal('testing');
});
});
describe('setWarning', () => {
it('sets the warning', () => {
store.setWarning(-999);
expect(store.warning).to.equal(-999);
});
it('clears the warning with no parameters', () => {
store.setWarning(-999);
store.setWarning();
expect(store.warning).to.equal(WARNING_NONE);
});
});
describe('toggleAcceptTerms', () => {
it('changes state on hasAcceptedTerms', () => {
store.toggleAcceptTerms();
expect(store.hasAcceptedTerms).to.be.true;
});
});
});
describe('operations', () => {
describe('getCoinPrice', () => {
beforeEach(() => {
sinon.stub(store._shapeshiftApi, 'getMarketInfo').resolves('testPrice');
return store.getCoinPrice();
});
afterEach(() => {
store._shapeshiftApi.getMarketInfo.restore();
});
it('retrieves the market info from ShapeShift', () => {
expect(store._shapeshiftApi.getMarketInfo).to.have.been.calledWith('btc_eth');
});
it('stores the price retrieved', () => {
expect(store.price).to.equal('testPrice');
});
it('sets a warning on failure', () => {
store._shapeshiftApi.getMarketInfo.restore();
sinon.stub(store._shapeshiftApi, 'getMarketInfo').rejects('someError');
return store.getCoinPrice().then(() => {
expect(store.warning).to.equal(WARNING_NO_PRICE);
});
});
});
describe('retrieveCoins', () => {
beforeEach(() => {
sinon.stub(store._shapeshiftApi, 'getCoins').resolves({
BTC: { symbol: 'BTC', status: 'available' },
ETC: { symbol: 'ETC' },
XMR: { symbol: 'XMR', status: 'available' }
});
sinon.stub(store, 'getCoinPrice');
return store.retrieveCoins();
});
afterEach(() => {
store._shapeshiftApi.getCoins.restore();
store.getCoinPrice.restore();
});
it('retrieves the coins from ShapeShift', () => {
expect(store._shapeshiftApi.getCoins).to.have.been.called;
});
it('sets the available coins', () => {
expect(store.coins.peek()).to.deep.equal([
{ status: 'available', symbol: 'BTC' },
{ status: 'available', symbol: 'XMR' }
]);
});
it('retrieves the price once resolved', () => {
expect(store.getCoinPrice).to.have.been.called;
});
});
describe('shift', () => {
beforeEach(() => {
sinon.stub(store, 'subscribe').resolves();
sinon.stub(store._shapeshiftApi, 'shift').resolves({ deposit: 'depositAddress' });
store.setRefundAddress('refundAddress');
return store.shift();
});
afterEach(() => {
store.subscribe.restore();
store._shapeshiftApi.shift.restore();
});
it('moves to stage STAGE_WAIT_DEPOSIT', () => {
expect(store.stage).to.equal(STAGE_WAIT_DEPOSIT);
});
it('calls ShapeShift with the correct parameters', () => {
expect(store._shapeshiftApi.shift).to.have.been.calledWith(ADDRESS, 'refundAddress', store.coinPair);
});
it('sets the depositAddress', () => {
expect(store.depositAddress).to.equal('depositAddress');
});
it('subscribes to updates', () => {
expect(store.subscribe).to.have.been.called;
});
it('sets error when shift fails', () => {
store._shapeshiftApi.shift.restore();
sinon.stub(store._shapeshiftApi, 'shift').rejects({ message: 'testingError' });
return store.shift().then(() => {
expect(store.error).to.match(/testingError/);
});
});
});
describe('subscribe', () => {
beforeEach(() => {
sinon.stub(store._shapeshiftApi, 'subscribe');
store.setDepositAddress('depositAddress');
return store.subscribe();
});
afterEach(() => {
store._shapeshiftApi.subscribe.restore();
});
it('calls into the ShapeShift subscribe', () => {
expect(store._shapeshiftApi.subscribe).to.have.been.calledWith('depositAddress', store.onExchangeInfo);
});
describe('onExchangeInfo', () => {
it('sets the error when fatal error retrieved', () => {
store.onExchangeInfo({ fatal: true, message: 'testing' });
expect(store.error.message).to.equal('testing');
});
it('does not set the error when non-fatal error retrieved', () => {
store.onExchangeInfo({ message: 'testing' });
expect(store.error).to.be.null;
});
describe('status received', () => {
const INFO = { status: 'received' };
beforeEach(() => {
store.onExchangeInfo(null, INFO);
});
it('sets the depositInfo', () => {
expect(store.depositInfo).to.deep.equal(INFO);
});
it('only advanced depositInfo once', () => {
store.onExchangeInfo(null, Object.assign({}, INFO, { state: 'secondTime' }));
expect(store.depositInfo).to.deep.equal(INFO);
});
});
describe('status completed', () => {
const INFO = { status: 'complete' };
beforeEach(() => {
store.onExchangeInfo(null, INFO);
});
it('sets the depositInfo', () => {
expect(store.exchangeInfo).to.deep.equal(INFO);
});
it('only advanced depositInfo once', () => {
store.onExchangeInfo(null, Object.assign({}, INFO, { state: 'secondTime' }));
expect(store.exchangeInfo).to.deep.equal(INFO);
});
});
});
});
describe('unsubscribe', () => {
beforeEach(() => {
sinon.stub(store._shapeshiftApi, 'unsubscribe');
store.setDepositAddress('depositAddress');
return store.unsubscribe();
});
afterEach(() => {
store._shapeshiftApi.unsubscribe.restore();
});
it('calls into the ShapeShift unsubscribe', () => {
expect(store._shapeshiftApi.unsubscribe).to.have.been.calledWith('depositAddress');
});
});
});
});

View File

@@ -113,6 +113,7 @@ const routes = [
{ path: 'apps', component: Dapps },
{ path: 'app/:id', component: Dapp },
{ path: 'web', component: Web },
{ path: 'web/:url', component: Web },
{ path: 'signer', component: Signer }
]
}

View File

@@ -25,6 +25,7 @@ import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
import DeleteIcon from 'material-ui/svg-icons/action/delete';
import DoneIcon from 'material-ui/svg-icons/action/done-all';
import EditIcon from 'material-ui/svg-icons/content/create';
import LinkIcon from 'material-ui/svg-icons/content/link';
import LockedIcon from 'material-ui/svg-icons/action/lock';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
@@ -47,6 +48,7 @@ export {
DeleteIcon,
DoneIcon,
EditIcon,
LinkIcon,
LockedIcon,
NextIcon,
PrevIcon,

View File

@@ -16,10 +16,12 @@
import BigNumber from 'bignumber.js';
import util from '~/api/util';
import apiutil from '~/api/util';
import { NULL_ADDRESS } from './constants';
// TODO: Convert to FormattedMessages as soon as comfortable with the impact, i.e. errors
// not being concatted into strings in components, all supporting a non-string format
export const ERRORS = {
invalidAddress: 'address is an invalid network address',
invalidAmount: 'the supplied amount should be a valid positive number',
@@ -42,9 +44,14 @@ export function validateAbi (abi) {
try {
abiParsed = JSON.parse(abi);
if (!util.isArray(abiParsed)) {
if (!apiutil.isArray(abiParsed)) {
abiError = ERRORS.invalidAbi;
return { abi, abiError, abiParsed };
return {
abi,
abiError,
abiParsed
};
}
// Validate each elements of the Array
@@ -54,8 +61,15 @@ export function validateAbi (abi) {
if (invalidIndex !== -1) {
const invalid = abiParsed[invalidIndex];
// TODO: Needs seperate error when using FormattedMessage (no concats)
abiError = `${ERRORS.invalidAbi} (#${invalidIndex}: ${invalid.name || invalid.type})`;
return { abi, abiError, abiParsed };
return {
abi,
abiError,
abiParsed
};
}
abi = JSON.stringify(abiParsed);
@@ -76,7 +90,7 @@ function isValidAbiFunction (object) {
}
return ((object.type === 'function' && object.name) || object.type === 'constructor') &&
(object.inputs && util.isArray(object.inputs));
(object.inputs && apiutil.isArray(object.inputs));
}
function isAbiFallback (object) {
@@ -94,7 +108,7 @@ function isValidAbiEvent (object) {
return (object.type === 'event') &&
(object.name) &&
(object.inputs && util.isArray(object.inputs));
(object.inputs && apiutil.isArray(object.inputs));
}
export function validateAddress (address) {
@@ -102,10 +116,10 @@ export function validateAddress (address) {
if (!address) {
addressError = ERRORS.invalidAddress;
} else if (!util.isAddressValid(address)) {
} else if (!apiutil.isAddressValid(address)) {
addressError = ERRORS.invalidAddress;
} else {
address = util.toChecksumAddress(address);
address = apiutil.toChecksumAddress(address);
}
return {
@@ -114,12 +128,12 @@ export function validateAddress (address) {
};
}
export function validateCode (code, api) {
export function validateCode (code) {
let codeError = null;
if (!code.length) {
if (!code || !code.length) {
codeError = ERRORS.invalidCode;
} else if (!api.util.isHex(code)) {
} else if (!apiutil.isHex(code)) {
codeError = ERRORS.invalidCode;
}
@@ -130,7 +144,9 @@ export function validateCode (code, api) {
}
export function validateName (name) {
const nameError = !name || name.trim().length < 2 ? ERRORS.invalidName : null;
const nameError = !name || name.trim().length < 2
? ERRORS.invalidName
: null;
return {
name,
@@ -162,6 +178,7 @@ export function validateUint (value) {
try {
const bn = new BigNumber(value);
if (bn.lt(0)) {
valueError = ERRORS.negativeNumber;
} else if (!bn.isInteger()) {

View File

@@ -0,0 +1,303 @@
// 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/>.
import BigNumber from 'bignumber.js';
import { NULL_ADDRESS } from './constants';
import { ERRORS, isNullAddress, validateAbi, validateAddress, validateCode, validateName, validatePositiveNumber, validateUint } from './validation';
describe('util/validation', () => {
describe('validateAbi', () => {
it('passes on valid ABI', () => {
const abi = '[{"type":"function","name":"test","inputs":[],"outputs":[]}]';
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: null,
abiParsed: [{
type: 'function',
name: 'test',
inputs: [],
outputs: []
}]
});
});
it('passes on valid ABI & trims ABI', () => {
const abi = '[ { "type" : "function" , "name" : "test" , "inputs" : [] , "outputs" : [] } ]';
expect(validateAbi(abi)).to.deep.equal({
abi: '[{"type":"function","name":"test","inputs":[],"outputs":[]}]',
abiError: null,
abiParsed: [{
type: 'function',
name: 'test',
inputs: [],
outputs: []
}]
});
});
it('sets error on invalid JSON', () => {
const abi = 'this is not json';
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: ERRORS.invalidAbi,
abiParsed: null
});
});
it('sets error on non-array JSON', () => {
const abi = '{}';
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: ERRORS.invalidAbi,
abiParsed: {}
});
});
it('fails with invalid event', () => {
const abi = '[{ "type":"event" }]';
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: `${ERRORS.invalidAbi} (#0: event)`,
abiParsed: [{ type: 'event' }]
});
});
it('fails with invalid function', () => {
const abi = '[{ "type":"function" }]';
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: `${ERRORS.invalidAbi} (#0: function)`,
abiParsed: [{ type: 'function' }]
});
});
it('fails with unknown type', () => {
const abi = '[{ "type":"somethingElse" }]';
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: `${ERRORS.invalidAbi} (#0: somethingElse)`,
abiParsed: [{ type: 'somethingElse' }]
});
});
});
describe('validateAddress', () => {
it('validates address', () => {
const address = '0x1234567890123456789012345678901234567890';
expect(validateAddress(address)).to.deep.equal({
address,
addressError: null
});
});
it('validates address and converts to checksum', () => {
const address = '0x5A5eFF38DA95b0D58b6C616f2699168B480953C9';
expect(validateAddress(address.toLowerCase())).to.deep.equal({
address,
addressError: null
});
});
it('sets error on null addresses', () => {
expect(validateAddress(null)).to.deep.equal({
address: null,
addressError: ERRORS.invalidAddress
});
});
it('sets error on invalid addresses', () => {
const address = '0x12344567';
expect(validateAddress(address)).to.deep.equal({
address,
addressError: ERRORS.invalidAddress
});
});
});
describe('validateCode', () => {
it('validates hex code', () => {
expect(validateCode('0x123abc')).to.deep.equal({
code: '0x123abc',
codeError: null
});
});
it('validates hex code (non-prefix)', () => {
expect(validateCode('123abc')).to.deep.equal({
code: '123abc',
codeError: null
});
});
it('sets error on invalid code', () => {
expect(validateCode(null)).to.deep.equal({
code: null,
codeError: ERRORS.invalidCode
});
});
it('sets error on empty code', () => {
expect(validateCode('')).to.deep.equal({
code: '',
codeError: ERRORS.invalidCode
});
});
it('sets error on non-hex code', () => {
expect(validateCode('123hfg')).to.deep.equal({
code: '123hfg',
codeError: ERRORS.invalidCode
});
});
});
describe('validateName', () => {
it('validates names', () => {
expect(validateName('Joe Bloggs')).to.deep.equal({
name: 'Joe Bloggs',
nameError: null
});
});
it('sets error on null names', () => {
expect(validateName(null)).to.deep.equal({
name: null,
nameError: ERRORS.invalidName
});
});
it('sets error on short names', () => {
expect(validateName(' 1 ')).to.deep.equal({
name: ' 1 ',
nameError: ERRORS.invalidName
});
});
});
describe('validatePositiveNumber', () => {
it('validates numbers', () => {
expect(validatePositiveNumber(123)).to.deep.equal({
number: 123,
numberError: null
});
});
it('validates strings', () => {
expect(validatePositiveNumber('123')).to.deep.equal({
number: '123',
numberError: null
});
});
it('validates bignumbers', () => {
expect(validatePositiveNumber(new BigNumber(123))).to.deep.equal({
number: new BigNumber(123),
numberError: null
});
});
it('sets error on invalid numbers', () => {
expect(validatePositiveNumber(null)).to.deep.equal({
number: null,
numberError: ERRORS.invalidAmount
});
});
it('sets error on negative numbers', () => {
expect(validatePositiveNumber(-1)).to.deep.equal({
number: -1,
numberError: ERRORS.invalidAmount
});
});
});
describe('validateUint', () => {
it('validates numbers', () => {
expect(validateUint(123)).to.deep.equal({
value: 123,
valueError: null
});
});
it('validates strings', () => {
expect(validateUint('123')).to.deep.equal({
value: '123',
valueError: null
});
});
it('validates bignumbers', () => {
expect(validateUint(new BigNumber(123))).to.deep.equal({
value: new BigNumber(123),
valueError: null
});
});
it('sets error on invalid numbers', () => {
expect(validateUint(null)).to.deep.equal({
value: null,
valueError: ERRORS.invalidNumber
});
});
it('sets error on negative numbers', () => {
expect(validateUint(-1)).to.deep.equal({
value: -1,
valueError: ERRORS.negativeNumber
});
});
it('sets error on decimal numbers', () => {
expect(validateUint(3.1415927)).to.deep.equal({
value: 3.1415927,
valueError: ERRORS.decimalNumber
});
});
});
describe('isNullAddress', () => {
it('verifies a prefixed null address', () => {
expect(isNullAddress(`0x${NULL_ADDRESS}`)).to.be.true;
});
it('verifies a non-prefixed null address', () => {
expect(isNullAddress(NULL_ADDRESS)).to.be.true;
});
it('sets false on a null value', () => {
expect(isNullAddress(null)).to.be.false;
});
it('sets false on a non-full length 00..00 value', () => {
expect(isNullAddress(NULL_ADDRESS.slice(2))).to.be.false;
});
it('sets false on a valid addess, non 00..00 value', () => {
expect(isNullAddress('0x1234567890123456789012345678901234567890')).to.be.false;
});
});
});

View File

@@ -0,0 +1,17 @@
// 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 default from './urlButton';

View File

@@ -0,0 +1,20 @@
/* 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/>.
*/
.button {
vertical-align: middle;
}

View File

@@ -0,0 +1,96 @@
// 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/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { withRouter } from 'react-router';
import Button from '~/ui/Button';
import { LinkIcon } from '~/ui/Icons';
import Input from '~/ui/Form/Input';
import styles from './urlButton.css';
const INPUT_STYLE = { display: 'inline-block', width: '20em' };
class UrlButton extends Component {
static propTypes = {
router: PropTypes.object.isRequired // injected by withRouter
};
state = {
inputShown: false
};
render () {
const { inputShown } = this.state;
return (
<div>
{ inputShown ? this.renderInput() : null }
<Button
className={ styles.button }
icon={ <LinkIcon /> }
label={
<FormattedMessage
id='dapps.button.url.label'
defaultMessage='URL' />
}
onClick={ this.toggleInput }
/>
</div>
);
}
renderInput () {
return (
<Input
hint={
<FormattedMessage
id='dapps.button.url.input'
defaultMessage='https://mkr.market' />
}
onBlur={ this.hideInput }
onFocus={ this.showInput }
onSubmit={ this.inputOnSubmit }
style={ INPUT_STYLE }
/>
);
}
toggleInput = () => {
const { inputShown } = this.state;
this.setState({
inputShown: !inputShown
});
}
hideInput = () => {
this.setState({ inputShown: false });
}
showInput = () => {
this.setState({ inputShown: true });
}
inputOnSubmit = (url) => {
const { router } = this.props;
router.push(`/web/${encodeURIComponent(url)}`);
}
}
export default withRouter(UrlButton);

View File

@@ -27,6 +27,7 @@ import PermissionStore from '~/modals/DappPermissions/store';
import { Actionbar, Button, Page } from '~/ui';
import { LockedIcon, VisibleIcon } from '~/ui/Icons';
import UrlButton from './UrlButton';
import DappsStore from './dappsStore';
import Summary from './Summary';
@@ -88,6 +89,7 @@ class Dapps extends Component {
defaultMessage='Decentralized Applications' />
}
buttons={ [
<UrlButton key='url' />,
<Button
icon={ <VisibleIcon /> }
key='edit'

View File

@@ -16,6 +16,8 @@
import React, { Component, PropTypes } from 'react';
import store from 'store';
import { parse as parseUrl, format as formatUrl } from 'url';
import { parse as parseQuery } from 'querystring';
import AddressBar from './AddressBar';
@@ -23,39 +25,53 @@ import styles from './web.css';
const LS_LAST_ADDRESS = '_parity::webLastAddress';
const hasProtocol = /^https?:\/\//;
export default class Web extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
params: PropTypes.object.isRequired
}
state = {
displayedUrl: this.lastAddress(),
displayedUrl: null,
isLoading: true,
token: null,
url: this.lastAddress()
url: null
};
componentDidMount () {
this.context.api.signer.generateWebProxyAccessToken().then(token => {
this.setState({ token });
});
const { api } = this.context;
const { params } = this.props;
api
.signer
.generateWebProxyAccessToken()
.then((token) => {
this.setState({ token });
});
this.setUrl(params.url);
}
address () {
const { dappsUrl } = this.context.api;
const { url, token } = this.state;
const path = url.replace(/:/g, '').replace(/\/\//g, '/');
return `${dappsUrl}/web/${token}/${path}/`;
componentWillReceiveProps (props) {
this.setUrl(props.params.url);
}
lastAddress () {
return store.get(LS_LAST_ADDRESS) || 'https://mkr.market';
}
setUrl = (url) => {
url = url || store.get(LS_LAST_ADDRESS) || 'https://mkr.market';
if (!hasProtocol.test(url)) {
url = `https://${url}`;
}
this.setState({ url, displayedUrl: url });
};
render () {
const { displayedUrl, isLoading, token } = this.state;
const address = this.address();
if (!token) {
return (
@@ -67,20 +83,30 @@ export default class Web extends Component {
);
}
const { dappsUrl } = this.context.api;
const { url } = this.state;
if (!url || !token) {
return null;
}
const parsed = parseUrl(url);
const { protocol, host, path } = parsed;
const address = `${dappsUrl}/web/${token}/${protocol.slice(0, -1)}/${host}${path}`;
return (
<div className={ styles.wrapper }>
<AddressBar
className={ styles.url }
isLoading={ isLoading }
onChange={ this.handleUpdateUrl }
onRefresh={ this.handleOnRefresh }
onChange={ this.onUrlChange }
onRefresh={ this.onRefresh }
url={ displayedUrl }
/>
<iframe
className={ styles.frame }
frameBorder={ 0 }
name={ name }
onLoad={ this.handleIframeLoad }
onLoad={ this.iframeOnLoad }
sandbox='allow-forms allow-same-origin allow-scripts'
scrolling='auto'
src={ address } />
@@ -88,7 +114,11 @@ export default class Web extends Component {
);
}
handleUpdateUrl = (url) => {
onUrlChange = (url) => {
if (!hasProtocol.test(url)) {
url = `https://${url}`;
}
store.set(LS_LAST_ADDRESS, url);
this.setState({
@@ -98,18 +128,23 @@ export default class Web extends Component {
});
};
handleOnRefresh = (ev) => {
onRefresh = () => {
const { displayedUrl } = this.state;
const hasQuery = displayedUrl.indexOf('?') > 0;
const separator = hasQuery ? '&' : '?';
// Insert timestamp
// This is a hack to prevent caching.
const parsed = parseUrl(displayedUrl);
parsed.query = parseQuery(parsed.query);
parsed.query.t = Date.now().toString();
delete parsed.search;
this.setState({
isLoading: true,
url: `${displayedUrl}${separator}t=${Date.now()}`
url: formatUrl(parsed)
});
};
handleIframeLoad = (ev) => {
iframeOnLoad = () => {
this.setState({
isLoading: false
});

View File

@@ -458,23 +458,65 @@ class WriteContract extends Component {
const { bytecode } = contract;
const abi = contract.interface;
const metadata = contract.metadata
? (
<Input
allowCopy
label='Metadata'
readOnly
value={ contract.metadata }
/>
)
: null;
return (
<div>
<Input
allowCopy
label='ABI Interface'
readOnly
value={ abi }
label='ABI Interface'
/>
<Input
allowCopy
label='Bytecode'
readOnly
value={ `0x${bytecode}` }
label='Bytecode'
/>
{ metadata }
{ this.renderSwarmHash(contract) }
</div>
);
}
renderSwarmHash (contract) {
if (!contract || !contract.metadata) {
return null;
}
const { bytecode } = contract;
// @see https://solidity.readthedocs.io/en/develop/miscellaneous.html#encoding-of-the-metadata-hash-in-the-bytecode
const hashRegex = /a165627a7a72305820([a-f0-9]{64})0029$/;
if (!hashRegex.test(bytecode)) {
return null;
}
const hash = hashRegex.exec(bytecode)[1];
return (
<Input
allowCopy
label='Swarm Metadata Hash'
readOnly
value={ `${hash}` }
/>
);
}
renderErrors () {
const { annotations } = this.store;