Minimise transactions progress (#4942)

* Watch the requests and display them throughout the app

* Linting

* Showing Requests

* Fully working Transaction Requests Display

* Add FormattedMessage to Requests

* Clean-up the Transfer dialog

* Update Validations

* Cleanup Create Wallet

* Clean Deploy Contract Dialog

* Cleanup Contract Execution

* Fix Requests

* Cleanup Wallet Settings

* Don't show stepper in Portal if less than 2 steps

* WIP local storage requests

* Caching requests and saving contract deployments

* Add Historic prop to Requests MethodDecoding

* Fix tests

* Add Contract address to MethodDecoding

* PR Grumbles - Part I

* PR Grumbles - Part II

* Use API Subscription methods

* Linting

* Move SavedRequests and add tests

* Added tests for Requests Actions

* Fixing tests

* PR Grumbles + Playground fix

* Revert Playground changes

* PR Grumbles

* Better showEth in MethodDecoding
This commit is contained in:
Nicolas Gotchac 2017-03-28 14:34:31 +02:00 committed by Jaco Greeff
parent e28c477075
commit a99721004b
40 changed files with 1382 additions and 1216 deletions

View File

@ -139,15 +139,16 @@ export function inOptionsCondition (condition) {
return condition;
}
export function inOptions (options) {
if (options) {
export function inOptions (_options = {}) {
const options = { ..._options };
Object.keys(options).forEach((key) => {
switch (key) {
case 'to':
// Don't encode the `to` option if it's empty
// (eg. contract deployments)
if (options[key]) {
options[key] = inAddress(options[key]);
options.to = inAddress(options[key]);
}
break;
@ -178,7 +179,6 @@ export function inOptions (options) {
break;
}
});
}
return options;
}

View File

@ -380,7 +380,7 @@ export default class Parity {
.execute('parity_postSign', inAddress(address), inHex(hash));
}
postTransaction (options) {
postTransaction (options = {}) {
return this._transport
.execute('parity_postTransaction', inOptions(options));
}

View File

@ -27,6 +27,7 @@ const events = {
'parity_accountsInfo': { module: 'personal' },
'parity_allAccountsInfo': { module: 'personal' },
'parity_defaultAccount': { module: 'personal' },
'parity_postTransaction': { module: 'signer' },
'eth_accounts': { module: 'personal' },
'signer_requestsToConfirm': { module: 'signer' }
};
@ -83,7 +84,7 @@ export default class Manager {
if (!engine.isStarted) {
engine.start();
} else {
} else if (error !== null || data !== null) {
this._sendData(subscriptionId, error, data);
}

View File

@ -124,7 +124,7 @@ describe('api/subscriptions/manager', () => {
});
});
it('does not call the callback after unsibscription', () => {
it('does not call the callback after unsubscription', () => {
expect(cb).to.have.been.calledWith(null, 'test');
expect(cb).to.not.have.been.calledWith(null, 'test2');
});

View File

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { outTransaction } from '../format/output';
export default class Signer {
constructor (updateSubscriptions, api, subscriber) {
this._subscriber = subscriber;
@ -58,6 +60,15 @@ export default class Signer {
.catch(nextTimeout);
}
_postTransaction (data) {
const request = {
transaction: outTransaction(data.params[0]),
requestId: data.json.result.result
};
this._updateSubscriptions('parity_postTransaction', null, request);
}
_loggingSubscribe () {
return this._subscriber.subscribe('logging', (error, data) => {
if (error || !data) {
@ -65,11 +76,15 @@ export default class Signer {
}
switch (data.method) {
case 'parity_postTransaction':
case 'eth_sendTranasction':
case 'eth_sendTransaction':
case 'eth_sendRawTransaction':
this._listRequests(false);
return;
case 'parity_postTransaction':
this._postTransaction(data);
this._listRequests(false);
return;
}
});
}

View File

@ -17,9 +17,12 @@
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { BusyStep, Button, Portal, TxHash } from '~/ui';
import { Button, Portal } from '~/ui';
import { CancelIcon, DoneIcon, NextIcon } from '~/ui/Icons';
import { setRequest } from '~/redux/providers/requestsActions';
import WalletType from './WalletType';
import WalletDetails from './WalletDetails';
@ -27,56 +30,25 @@ import WalletInfo from './WalletInfo';
import CreateWalletStore from './createWalletStore';
@observer
export default class CreateWallet extends Component {
export class CreateWallet extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired
onClose: PropTypes.func.isRequired,
onSetRequest: PropTypes.func.isRequired
};
store = new CreateWalletStore(this.context.api, this.props.accounts);
store = new CreateWalletStore(this.context.api, this.props);
render () {
const { stage, steps, waiting, rejected } = this.store;
if (rejected) {
return (
<Portal
buttons={ this.renderDialogActions() }
onClose={ this.onClose }
open
title={
<FormattedMessage
id='createWallet.rejected.title'
defaultMessage='rejected'
/>
}
>
<BusyStep
title={
<FormattedMessage
id='createWallet.rejected.message'
defaultMessage='The deployment has been rejected'
/>
}
state={
<FormattedMessage
id='createWallet.rejected.state'
defaultMessage='The wallet will not be created. You can safely close this window.'
/>
}
/>
</Portal>
);
}
const { stage, steps } = this.store;
return (
<Portal
activeStep={ stage }
busySteps={ waiting }
buttons={ this.renderDialogActions() }
onClose={ this.onClose }
open
@ -92,25 +64,6 @@ export default class CreateWallet extends Component {
const { accounts } = this.props;
switch (step) {
case 'DEPLOYMENT':
return (
<BusyStep
title={
<FormattedMessage
id='createWallet.deployment.message'
defaultMessage='The deployment is currently in progress'
/>
}
state={ this.store.deployState }
>
{
this.store.txhash
? <TxHash hash={ this.store.txhash } />
: null
}
</BusyStep>
);
case 'INFO':
return (
<WalletInfo
@ -148,7 +101,7 @@ export default class CreateWallet extends Component {
}
renderDialogActions () {
const { step, hasErrors, rejected, onCreate, onNext, onAdd } = this.store;
const { step, hasErrors, onCreate, onNext, onAdd } = this.store;
const cancelBtn = (
<Button
@ -164,20 +117,6 @@ export default class CreateWallet extends Component {
/>
);
const closeBtn = (
<Button
icon={ <CancelIcon /> }
key='close'
label={
<FormattedMessage
id='createWallet.button.close'
defaultMessage='Close'
/>
}
onClick={ this.onClose }
/>
);
const doneBtn = (
<Button
icon={ <DoneIcon /> }
@ -192,20 +131,6 @@ export default class CreateWallet extends Component {
/>
);
const sendingBtn = (
<Button
icon={ <DoneIcon /> }
key='sending'
label={
<FormattedMessage
id='createWallet.button.sending'
defaultMessage='Sending...'
/>
}
disabled
/>
);
const nextBtn = (
<Button
icon={ <NextIcon /> }
@ -220,14 +145,7 @@ export default class CreateWallet extends Component {
/>
);
if (rejected) {
return [ closeBtn ];
}
switch (step) {
case 'DEPLOYMENT':
return [ closeBtn, sendingBtn ];
case 'INFO':
return [ doneBtn ];
@ -274,3 +192,14 @@ export default class CreateWallet extends Component {
this.props.onClose();
}
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
onSetRequest: setRequest
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(CreateWallet);

View File

@ -18,7 +18,7 @@ import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import CreateWallet from './';
import { CreateWallet } from './createWallet';
import { ACCOUNTS } from './createWallet.test.js';
@ -47,7 +47,7 @@ function render () {
return component;
}
describe('CreateWallet', () => {
describe('modals/CreateWallet', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});

View File

@ -14,12 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { noop } from 'lodash';
import { observable, computed, action, transaction } from 'mobx';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Contract from '~/api/contract';
import { ERROR_CODES } from '~/api/transport/error';
import Contracts from '~/contracts';
import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode, walletLibrary as walletLibraryCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet';
@ -46,15 +46,6 @@ const STEPS = {
/>
)
},
DEPLOYMENT: {
title: (
<FormattedMessage
id='createWallet.steps.deployment'
defaultMessage='wallet deployment'
/>
),
waiting: true
},
INFO: {
title: (
<FormattedMessage
@ -67,13 +58,8 @@ const STEPS = {
export default class CreateWalletStore {
@observable step = null;
@observable rejected = false;
@observable deployState = null;
@observable deployError = null;
@observable deployed = false;
@observable txhash = null;
@observable walletType = 'MULTISIG';
@observable wallet = {
account: '',
@ -85,7 +71,6 @@ export default class CreateWalletStore {
name: '',
description: ''
};
@observable walletType = 'MULTISIG';
@observable errors = {
account: null,
@ -96,6 +81,9 @@ export default class CreateWalletStore {
name: null
};
onClose = noop;
onSetRequest = noop;
@computed get stage () {
return this.stepsKeys.findIndex((k) => k === this.step);
}
@ -125,24 +113,17 @@ export default class CreateWalletStore {
key
};
})
.filter((step) => {
return (this.walletType !== 'WATCH' || step.key !== 'DEPLOYMENT');
});
.filter((step) => this.walletType === 'WATCH' || step.key !== 'INFO');
}
@computed get waiting () {
this.steps
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
}
constructor (api, accounts) {
constructor (api, { accounts, onClose, onSetRequest }) {
this.api = api;
this.step = this.stepsKeys[0];
this.wallet.account = Object.values(accounts)[0].address;
this.validateWallet(this.wallet);
this.onClose = onClose;
this.onSetRequest = onSetRequest;
}
@action onTypeChange = (type) => {
@ -193,8 +174,6 @@ export default class CreateWalletStore {
return;
}
this.step = 'DEPLOYMENT';
const { account, owners, required, daylimit } = this.wallet;
Contracts
@ -243,25 +222,13 @@ export default class CreateWalletStore {
const contract = this.api.newContract(walletAbi);
this.wallet = this.getWalletWithMeta(this.wallet);
return deploy(contract, options, [ owners, required, daylimit ], this.wallet.metadata, this.onDeploymentState);
})
.then((address) => {
if (!address || /^(0x)?0*$/.test(address)) {
return false;
}
this.onClose();
return deploy(contract, options, [ owners, required, daylimit ])
.then((requestId) => {
const metadata = { ...this.wallet.metadata, deployment: true };
this.deployed = true;
this.wallet.address = address;
return this.addWallet(this.wallet);
})
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.rejected = true;
return;
}
console.error('error deploying contract', error);
this.deployError = error;
this.onSetRequest(requestId, { metadata }, false);
});
});
}
@ -297,75 +264,6 @@ export default class CreateWalletStore {
};
}
onDeploymentState = (error, data) => {
if (error) {
return console.error('createWallet::onDeploymentState', error);
}
switch (data.state) {
case 'estimateGas':
case 'postTransaction':
this.deployState = (
<FormattedMessage
id='createWallet.states.preparing'
defaultMessage='Preparing transaction for network transmission'
/>
);
return;
case 'checkRequest':
this.deployState = (
<FormattedMessage
id='createWallet.states.waitingConfirm'
defaultMessage='Waiting for confirmation of the transaction in the Parity Secure Signer'
/>
);
return;
case 'getTransactionReceipt':
this.deployState = (
<FormattedMessage
id='createWallet.states.waitingReceipt'
defaultMessage='Waiting for the contract deployment transaction receipt'
/>
);
this.txhash = data.txhash;
return;
case 'hasReceipt':
case 'getCode':
this.deployState = (
<FormattedMessage
id='createWallet.states.validatingCode'
defaultMessage='Validating the deployed contract code'
/>
);
return;
case 'confirmationNeeded':
this.deployState = (
<FormattedMessage
id='createWallet.states.confirmationNeeded'
defaultMessage='The contract deployment needs confirmations from other owners of the Wallet'
/>
);
return;
case 'completed':
this.deployState = (
<FormattedMessage
id='createWallet.states.completed'
defaultMessage='The contract deployment has been completed'
/>
);
return;
default:
console.error('createWallet::onDeploymentState', 'unknow contract deployment state', data);
return;
}
}
@action validateWallet = (_wallet) => {
const addressValidation = validateAddress(_wallet.address);
const accountValidation = validateAddress(_wallet.account);

View File

@ -15,19 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.center {
text-align: center;
}
.address {
vertical-align: top;
display: inline-block;
}
.identityicon {
margin: -8px 0.5em;
}
.funcparams {
padding-left: 3em;
}

View File

@ -20,21 +20,18 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { BusyStep, Button, CompletedStep, CopyToClipboard, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui';
import { CancelIcon, DoneIcon } from '~/ui/Icons';
import { Button, GasPriceEditor, IdentityIcon, Portal, Warning } from '~/ui';
import { CancelIcon } from '~/ui/Icons';
import { ERRORS, validateAbi, validateCode, validateName, validatePositiveNumber } from '~/util/validation';
import { deploy, deployEstimateGas } from '~/util/tx';
import { setRequest } from '~/redux/providers/requestsActions';
import DetailsStep from './DetailsStep';
import ParametersStep from './ParametersStep';
import ErrorStep from './ErrorStep';
import Extras from '../Transfer/Extras';
import styles from './deployContract.css';
import { ERROR_CODES } from '~/api/transport/error';
const STEPS = {
CONTRACT_DETAILS: {
title: (
@ -59,23 +56,6 @@ const STEPS = {
defaultMessage='extra information'
/>
)
},
DEPLOYMENT: {
waiting: true,
title: (
<FormattedMessage
id='deployContract.title.deployment'
defaultMessage='deployment'
/>
)
},
COMPLETED: {
title: (
<FormattedMessage
id='deployContract.title.completed'
defaultMessage='completed'
/>
)
}
};
@ -93,6 +73,7 @@ class DeployContract extends Component {
code: PropTypes.string,
gasLimit: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
onSetRequest: PropTypes.func.isRequired,
readOnly: PropTypes.bool,
source: PropTypes.string
};
@ -112,8 +93,6 @@ class DeployContract extends Component {
amountError: '',
code: '',
codeError: ERRORS.invalidCode,
deployState: '',
deployError: null,
description: '',
descriptionError: null,
extras: false,
@ -124,9 +103,8 @@ class DeployContract extends Component {
params: [],
paramsError: [],
inputs: [],
rejected: false,
step: 'CONTRACT_DETAILS'
}
};
componentWillMount () {
const { abi, code } = this.props;
@ -154,11 +132,9 @@ class DeployContract extends Component {
}
render () {
const { step, deployError, rejected, inputs } = this.state;
const { step, inputs } = this.state;
const realStepKeys = deployError || rejected
? []
: Object.keys(STEPS)
const realStepKeys = Object.keys(STEPS)
.filter((k) => {
if (k === 'CONTRACT_PARAMETERS') {
return inputs.length > 0;
@ -172,45 +148,15 @@ class DeployContract extends Component {
});
const realStep = realStepKeys.findIndex((k) => k === step);
const realSteps = realStepKeys.length
? realStepKeys.map((k) => STEPS[k])
: null;
const title = realSteps
? null
: (
deployError
? (
<FormattedMessage
id='deployContract.title.failed'
defaultMessage='deployment failed'
/>
)
: (
<FormattedMessage
id='deployContract.title.rejected'
defaultMessage='rejected'
/>
)
);
const waiting = realSteps
? realSteps.map((s, i) => s.waiting ? i : false).filter((v) => v !== false)
: null;
const realSteps = realStepKeys.map((k) => STEPS[k]);
return (
<Portal
buttons={ this.renderDialogActions() }
activeStep={ realStep }
busySteps={ waiting }
onClose={ this.onClose }
open
steps={
realSteps
? realSteps.map((s) => s.title)
: null
}
title={ title }
steps={ realSteps.map((s) => s.title) }
>
{ this.renderExceptionWarning() }
{ this.renderStep() }
@ -264,20 +210,6 @@ class DeployContract extends Component {
/>
);
const closeBtnOk = (
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='deployContract.button.done'
defaultMessage='Done'
/>
}
onClick={ this.onClose }
/>
);
if (deployError) {
return closeBtn;
}
@ -346,43 +278,12 @@ class DeployContract extends Component {
cancelBtn,
createButton
];
case 'DEPLOYMENT':
return [ closeBtn ];
case 'COMPLETED':
return [ closeBtnOk ];
}
}
renderStep () {
const { accounts, readOnly, balances } = this.props;
const { address, deployError, step, deployState, txhash, rejected } = this.state;
if (deployError) {
return (
<ErrorStep error={ deployError } />
);
}
if (rejected) {
return (
<BusyStep
title={
<FormattedMessage
id='deployContract.rejected.title'
defaultMessage='The deployment has been rejected'
/>
}
state={
<FormattedMessage
id='deployContract.rejected.description'
defaultMessage='You can safely close this window, the contract deployment will not occur.'
/>
}
/>
);
}
const { step } = this.state;
switch (step) {
case 'CONTRACT_DETAILS':
@ -416,50 +317,6 @@ class DeployContract extends Component {
case 'EXTRAS':
return this.renderExtrasPage();
case 'DEPLOYMENT':
const body = txhash
? <TxHash hash={ txhash } />
: null;
return (
<BusyStep
title={
<FormattedMessage
id='deployContract.busy.title'
defaultMessage='The deployment is currently in progress'
/>
}
state={ deployState }
>
{ body }
</BusyStep>
);
case 'COMPLETED':
return (
<CompletedStep>
<div>
<FormattedMessage
id='deployContract.completed.description'
defaultMessage='Your contract has been deployed at'
/>
</div>
<div>
<CopyToClipboard data={ address } />
<IdentityIcon
address={ address }
center
className={ styles.identityicon }
inline
/>
<div className={ styles.address }>
{ address }
</div>
</div>
<TxHash hash={ txhash } />
</CompletedStep>
);
}
}
@ -591,7 +448,7 @@ class DeployContract extends Component {
}
onDeployStart = () => {
const { api, store } = this.context;
const { api } = this.context;
const { source } = this.props;
const { abiParsed, amountValue, code, description, name, params, fromAddress } = this.state;
@ -611,123 +468,15 @@ class DeployContract extends Component {
value: amountValue
});
this.setState({ step: 'DEPLOYMENT' });
const contract = api.newContract(abiParsed);
deploy(contract, options, params, metadata, this.onDeploymentState, true)
.then((address) => {
// No contract address given, might need some confirmations
// from the wallet owners...
if (!address || /^(0x)?0*$/.test(address)) {
return false;
}
this.onClose();
deploy(contract, options, params, true)
.then((requestId) => {
const requestMetadata = { ...metadata, deployment: true };
metadata.blockNumber = contract._receipt
? contract.receipt.blockNumber.toNumber()
: null;
return Promise.all([
api.parity.setAccountName(address, name),
api.parity.setAccountMeta(address, metadata)
])
.then(() => {
console.log(`contract deployed at ${address}`);
this.setState({ step: 'COMPLETED', address });
this.props.onSetRequest(requestId, { metadata: requestMetadata }, false);
});
})
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true });
return false;
}
console.error('error deploying contract', error);
this.setState({ deployError: error });
store.dispatch({ type: 'newError', error });
});
}
onDeploymentState = (error, data) => {
if (error) {
console.error('onDeploymentState', error);
return;
}
switch (data.state) {
case 'estimateGas':
case 'postTransaction':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.preparing'
defaultMessage='Preparing transaction for network transmission'
/>
)
});
return;
case 'checkRequest':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.waitSigner'
defaultMessage='Waiting for confirmation of the transaction in the Parity Secure Signer'
/>
)
});
return;
case 'getTransactionReceipt':
this.setState({
txhash: data.txhash,
deployState: (
<FormattedMessage
id='deployContract.state.waitReceipt'
defaultMessage='Waiting for the contract deployment transaction receipt'
/>
)
});
return;
case 'hasReceipt':
case 'getCode':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.validatingCode'
defaultMessage='Validating the deployed contract code'
/>
)
});
return;
case 'confirmationNeeded':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.confirmationNeeded'
defaultMessage='The operation needs confirmations from the other owners of the contract'
/>
)
});
return;
case 'completed':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.completed'
defaultMessage='The contract deployment has been completed'
/>
)
});
return;
default:
console.error('Unknown contract deployment state', data);
return;
}
}
onClose = () => {
@ -752,6 +501,13 @@ function mapStateToProps (initState, initProps) {
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
onSetRequest: setRequest
}, dispatch);
}
export default connect(
mapStateToProps
mapStateToProps,
mapDispatchToProps
)(DeployContract);

View File

@ -21,8 +21,8 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { toWei } from '~/api/util/wei';
import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui';
import { CancelIcon, DoneIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { Button, GasPriceEditor, IdentityIcon, Portal, Warning } from '~/ui';
import { CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { MAX_GAS_ESTIMATION } from '~/util/constants';
import { validateAddress, validateUint } from '~/util/validation';
import { parseAbiType } from '~/util/abi';
@ -30,11 +30,7 @@ import { parseAbiType } from '~/util/abi';
import AdvancedStep from './AdvancedStep';
import DetailsStep from './DetailsStep';
import { ERROR_CODES } from '~/api/transport/error';
const STEP_DETAILS = 0;
const STEP_BUSY_OR_ADVANCED = 1;
const STEP_BUSY = 2;
const TITLES = {
transfer: (
@ -43,40 +39,22 @@ const TITLES = {
defaultMessage='function details'
/>
),
sending: (
<FormattedMessage
id='executeContract.steps.sending'
defaultMessage='sending'
/>
),
complete: (
<FormattedMessage
id='executeContract.steps.complete'
defaultMessage='complete'
/>
),
advanced: (
<FormattedMessage
id='executeContract.steps.advanced'
defaultMessage='advanced options'
/>
),
rejected: (
<FormattedMessage
id='executeContract.steps.rejected'
defaultMessage='rejected'
/>
)
};
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
const STAGES_ADVANCED = [TITLES.transfer, TITLES.advanced, TITLES.sending, TITLES.complete];
const STAGES_BASIC = [TITLES.transfer];
const STAGES_ADVANCED = [TITLES.transfer, TITLES.advanced];
@observer
class ExecuteContract extends Component {
static contextTypes = {
api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired
}
};
static propTypes = {
accounts: PropTypes.object,
@ -86,7 +64,7 @@ class ExecuteContract extends Component {
gasLimit: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
onFromAddressChange: PropTypes.func.isRequired
}
};
gasStore = new GasPriceEditor.Store(this.context.api, { gasLimit: this.props.gasLimit });
@ -94,17 +72,13 @@ class ExecuteContract extends Component {
advancedOptions: false,
amount: '0',
amountError: null,
busyState: null,
fromAddressError: null,
func: null,
funcError: null,
rejected: false,
sending: false,
step: STEP_DETAILS,
txhash: null,
values: [],
valuesError: []
}
};
componentDidMount () {
const { contract } = this.props;
@ -122,23 +96,13 @@ class ExecuteContract extends Component {
}
render () {
const { advancedOptions, rejected, sending, step } = this.state;
const { advancedOptions, step } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
if (rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return (
<Portal
activeStep={ step }
buttons={ this.renderDialogActions() }
busySteps={
advancedOptions
? [STEP_BUSY]
: [STEP_BUSY_OR_ADVANCED]
}
busy={ sending }
onClose={ this.onClose }
open
steps={ steps }
@ -150,10 +114,9 @@ class ExecuteContract extends Component {
}
renderExceptionWarning () {
const { gasEdit, step } = this.state;
const { errorEstimated } = this.gasStore;
if (!errorEstimated || step >= (gasEdit ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
if (!errorEstimated) {
return null;
}
@ -164,8 +127,8 @@ class ExecuteContract extends Component {
renderDialogActions () {
const { fromAddress } = this.props;
const { advancedOptions, sending, step, fromAddressError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error);
const { advancedOptions, step, fromAddressError, valuesError } = this.state;
const hasError = !!(fromAddressError || valuesError.find((error) => error));
const cancelBtn = (
<Button
@ -189,7 +152,7 @@ class ExecuteContract extends Component {
defaultMessage='post transaction'
/>
}
disabled={ !!(sending || hasError) }
disabled={ hasError }
icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction }
/>
@ -226,11 +189,8 @@ class ExecuteContract extends Component {
cancelBtn,
advancedOptions ? nextBtn : postBtn
];
} else if (step === (advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
return [
cancelBtn
];
} else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
}
return [
cancelBtn,
prevBtn,
@ -238,43 +198,9 @@ class ExecuteContract extends Component {
];
}
return [
<Button
key='close'
label={
<FormattedMessage
id='executeContract.button.done'
defaultMessage='done'
/>
}
icon={ <DoneIcon /> }
onClick={ this.onClose }
/>
];
}
renderStep () {
const { onFromAddressChange } = this.props;
const { advancedOptions, step, busyState, txhash, rejected } = this.state;
if (rejected) {
return (
<BusyStep
title={
<FormattedMessage
id='executeContract.rejected.title'
defaultMessage='The execution has been rejected'
/>
}
state={
<FormattedMessage
id='executeContract.rejected.state'
defaultMessage='You can safely close this window, the function execution will not occur.'
/>
}
/>
);
}
const { step } = this.state;
if (step === STEP_DETAILS) {
return (
@ -288,28 +214,10 @@ class ExecuteContract extends Component {
onValueChange={ this.onValueChange }
/>
);
} else if (step === (advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
return (
<BusyStep
title={
<FormattedMessage
id='executeContract.busy.title'
defaultMessage='The function execution is in progress'
/>
}
state={ busyState }
/>
);
} else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
return (
<AdvancedStep gasStore={ this.gasStore } />
);
}
return (
<CompletedStep>
<TxHash hash={ txhash } />
</CompletedStep>
<AdvancedStep gasStore={ this.gasStore } />
);
}
@ -390,59 +298,17 @@ class ExecuteContract extends Component {
}
postTransaction = () => {
const { api, store } = this.context;
const { api } = this.context;
const { fromAddress } = this.props;
const { advancedOptions, amount, func, values } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
const finalstep = steps.length - 1;
const { amount, func, values } = this.state;
const options = this.gasStore.overrideTransaction({
from: fromAddress,
value: api.util.toWei(amount || 0)
});
this.setState({ sending: true, step: advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED });
func
.postTransaction(options, values)
.then((requestId) => {
this.setState({
busyState: (
<FormattedMessage
id='executeContract.busy.waitAuth'
defaultMessage='Waiting for authorization in the Parity Signer'
/>
)
});
return api
.pollMethod('parity_checkRequest', requestId)
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true, step: finalstep });
return false;
}
throw error;
});
})
.then((txhash) => {
this.setState({
sending: false,
step: finalstep,
txhash,
busyState: (
<FormattedMessage
id='executeContract.busy.posted'
defaultMessage='Your transaction has been posted to the network'
/>
)
});
})
.catch((error) => {
console.error('postTransaction', error);
store.dispatch({ type: 'newError', error });
});
func.postTransaction(options, values);
this.onClose();
}
onAdvancedClick = () => {

View File

@ -14,16 +14,14 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { noop } from 'lodash';
import { observable, computed, action, transaction } from 'mobx';
import BigNumber from 'bignumber.js';
import { uniq } from 'lodash';
import { wallet as walletAbi } from '~/contracts/abi';
import { bytesToHex } from '~/api/util/format';
import { fromWei } from '~/api/util/wei';
import Contract from '~/api/contract';
import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
import GasPriceStore from '~/ui/GasPriceEditor/store';
import { getLogger, LOG_KEYS } from '~/config';
@ -32,13 +30,10 @@ const log = getLogger(LOG_KEYS.TransferModalStore);
const TITLES = {
transfer: 'transfer details',
sending: 'sending',
complete: 'complete',
extras: 'extra information',
rejected: 'rejected'
extras: 'extra information'
};
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete];
const STAGES_BASIC = [TITLES.transfer];
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras];
export const WALLET_WARNING_SPENT_TODAY_LIMIT = 'WALLET_WARNING_SPENT_TODAY_LIMIT';
@ -49,8 +44,6 @@ export default class TransferStore {
@observable sending = false;
@observable tag = 'ETH';
@observable isEth = true;
@observable busyState = null;
@observable rejected = false;
@observable data = '';
@observable dataError = null;
@ -72,8 +65,8 @@ export default class TransferStore {
account = null;
balance = null;
onClose = null;
onClose = noop;
senders = null;
isWallet = false;
wallet = null;
@ -83,7 +76,7 @@ export default class TransferStore {
constructor (api, props) {
this.api = api;
const { account, balance, gasLimit, senders, newError, sendersBalances } = props;
const { account, balance, gasLimit, onClose, senders, newError, sendersBalances } = props;
this.account = account;
this.balance = balance;
@ -102,15 +95,15 @@ export default class TransferStore {
this.sendersBalances = sendersBalances;
this.senderError = ERRORS.requireSender;
}
if (onClose) {
this.onClose = onClose;
}
}
@computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
if (this.rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return steps;
}
@ -147,6 +140,7 @@ export default class TransferStore {
@action handleClose = () => {
this.stage = 0;
this.onClose();
}
@action onUpdateDetails = (type, value) => {
@ -186,82 +180,11 @@ export default class TransferStore {
this
.send()
.then((requestId) => {
this.busyState = 'Waiting for authorization in the Parity Signer';
return this.api
.pollMethod('parity_checkRequest', requestId)
.catch((e) => {
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
this.rejected = true;
return false;
}
throw e;
});
})
.then((txhash) => {
transaction(() => {
this.onNext();
this.sending = false;
this.txhash = txhash;
this.busyState = 'Your transaction has been posted to the network';
});
if (this.isWallet) {
return this._attachWalletOperation(txhash);
}
})
.catch((error) => {
this.sending = false;
this.newError(error);
});
}
@action _attachWalletOperation = (txhash) => {
if (!txhash || /^(0x)?0*$/.test(txhash)) {
return;
}
let ethSubscriptionId = null;
// Number of blocks left to look-up (unsub after 15 blocks if nothing)
let nBlocksLeft = 15;
return this.api.subscribe('eth_blockNumber', () => {
this.api.eth
.getTransactionReceipt(txhash)
.then((tx) => {
if (nBlocksLeft <= 0) {
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
return;
}
if (!tx) {
nBlocksLeft--;
return;
}
const logs = this.walletContract.parseEventLogs(tx.logs);
const operations = uniq(logs
.filter((log) => log && log.params && log.params.operation)
.map((log) => bytesToHex(log.params.operation.value)));
if (operations.length > 0) {
this.operation = operations[0];
}
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
})
.catch(() => {
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
});
}).then((subId) => {
ethSubscriptionId = subId;
.then(() => {
this.handleClose();
});
}

View File

@ -21,9 +21,9 @@ import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react';
import { pick } from 'lodash';
import { BusyStep, CompletedStep, Button, IdentityIcon, Input, Portal, TxHash, Warning } from '~/ui';
import { Button, IdentityIcon, Portal, Warning } from '~/ui';
import { newError } from '~/ui/Errors/actions';
import { CancelIcon, DoneIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { nullableProptype } from '~/util/proptypes';
import Details from './Details';
@ -33,8 +33,7 @@ import TransferStore, { WALLET_WARNING_SPENT_TODAY_LIMIT } from './store';
import styles from './transfer.css';
const STEP_DETAILS = 0;
const STEP_ADVANCED_OR_BUSY = 1;
const STEP_BUSY = 2;
const STEP_EXTRA = 1;
@observer
class Transfer extends Component {
@ -57,16 +56,11 @@ class Transfer extends Component {
store = new TransferStore(this.context.api, this.props);
render () {
const { stage, extras, steps } = this.store;
const { stage, steps } = this.store;
return (
<Portal
activeStep={ stage }
busySteps={
extras
? [STEP_BUSY]
: [STEP_ADVANCED_OR_BUSY]
}
buttons={ this.renderDialogActions() }
onClose={ this.handleClose }
open
@ -80,10 +74,9 @@ class Transfer extends Component {
}
renderExceptionWarning () {
const { extras, stage } = this.store;
const { errorEstimated } = this.store.gasStore;
if (!errorEstimated || stage >= (extras ? STEP_BUSY : STEP_ADVANCED_OR_BUSY)) {
if (!errorEstimated) {
return null;
}
@ -144,68 +137,9 @@ class Transfer extends Component {
if (stage === STEP_DETAILS) {
return this.renderDetailsPage();
} else if (stage === STEP_ADVANCED_OR_BUSY && extras) {
} else if (stage === STEP_EXTRA && extras) {
return this.renderExtrasPage();
}
return this.renderCompletePage();
}
renderCompletePage () {
const { sending, txhash, busyState, rejected } = this.store;
if (rejected) {
return (
<BusyStep
title='The transaction has been rejected'
state='You can safely close this window, the transfer will not occur.'
/>
);
}
if (sending) {
return (
<BusyStep
title='The transaction is in progress'
state={ busyState }
/>
);
}
return (
<CompletedStep>
<TxHash hash={ txhash } />
{
this.store.operation
? (
<div>
<br />
<div>
<p>
<FormattedMessage
id='transfer.wallet.confirmation'
defaultMessage='This transaction needs confirmation from other owners.'
/>
</p>
<Input
style={ { width: '50%', margin: '0 auto' } }
value={ this.store.operation }
label={
<FormattedMessage
id='transfer.wallet.operationHash'
defaultMessage='operation hash'
/>
}
readOnly
allowCopy
/>
</div>
</div>
)
: null
}
</CompletedStep>
);
}
renderDetailsPage () {
@ -319,19 +253,6 @@ class Transfer extends Component {
onClick={ this.store.onSend }
/>
);
const doneBtn = (
<Button
icon={ <DoneIcon /> }
key='close'
label={
<FormattedMessage
id='transfer.buttons.close'
defaultMessage='Close'
/>
}
onClick={ this.handleClose }
/>
);
switch (stage) {
case 0:
@ -339,19 +260,14 @@ class Transfer extends Component {
? [cancelBtn, nextBtn]
: [cancelBtn, sendBtn];
case 1:
return extras
? [cancelBtn, prevBtn, sendBtn]
: [doneBtn];
return [cancelBtn, prevBtn, sendBtn];
default:
return [doneBtn];
return [cancelBtn];
}
}
handleClose = () => {
const { onClose } = this.props;
this.store.handleClose();
typeof onClose === 'function' && onClose();
}
}

View File

@ -20,8 +20,8 @@ import { connect } from 'react-redux';
import { observer } from 'mobx-react';
import { pick } from 'lodash';
import { BusyStep, AddressSelect, Button, Form, TypedInput, Input, InputAddress, Portal, TxHash } from '~/ui';
import { CancelIcon, DoneIcon, NextIcon } from '~/ui/Icons';
import { AddressSelect, Button, Form, TypedInput, Input, InputAddress, Portal } from '~/ui';
import { CancelIcon, NextIcon } from '~/ui/Icons';
import { fromWei } from '~/api/util/wei';
import WalletSettingsStore from './walletSettingsStore.js';
@ -40,46 +40,14 @@ class WalletSettings extends Component {
senders: PropTypes.object.isRequired
};
store = new WalletSettingsStore(this.context.api, this.props.wallet);
store = new WalletSettingsStore(this.context.api, this.props);
render () {
const { stage, steps, waiting, rejected } = this.store;
if (rejected) {
return (
<Portal
onClose={ this.onClose }
open
title={
<FormattedMessage
id='walletSettings.rejected.title'
defaultMessage='rejected'
/>
}
actions={ this.renderDialogActions() }
>
<BusyStep
title={
<FormattedMessage
id='walletSettings.rejected.busyStep.title'
defaultMessage='The modifications have been rejected'
/>
}
state={
<FormattedMessage
id='walletSettings.rejected.busyStep.state'
defaultMessage='The wallet settings will not be modified. You can safely close this window.'
/>
}
/>
</Portal>
);
}
const { stage, steps } = this.store;
return (
<Portal
activeStep={ stage }
busySteps={ waiting }
buttons={ this.renderDialogActions() }
onClose={ this.onClose }
open
@ -94,43 +62,6 @@ class WalletSettings extends Component {
const { step } = this.store;
switch (step) {
case 'SENDING':
return (
<BusyStep
title='The modifications are currently being sent'
state={ this.store.deployState }
>
{
this.store.requests.map((req) => {
const key = req.id;
if (req.txhash) {
return (
<TxHash
key={ key }
hash={ req.txhash }
/>
);
}
if (req.rejected) {
return (
<p key={ key }>
<FormattedMessage
id='walletSettings.rejected'
defaultMessage='The transaction #{txid} has been rejected'
values={ {
txid: parseInt(key, 16)
} }
/>
</p>
);
}
})
}
</BusyStep>
);
case 'CONFIRMATION':
const { changes } = this.store;
@ -396,7 +327,7 @@ class WalletSettings extends Component {
}
renderDialogActions () {
const { step, hasErrors, rejected, onNext, send, done } = this.store;
const { step, hasErrors, onNext, send } = this.store;
const cancelBtn = (
<Button
@ -426,20 +357,6 @@ class WalletSettings extends Component {
/>
);
const sendingBtn = (
<Button
icon={ <DoneIcon /> }
key='sendingBtn'
label={
<FormattedMessage
id='walletSettings.buttons.sending'
defaultMessage='Sending...'
/>
}
disabled
/>
);
const nextBtn = (
<Button
icon={ <NextIcon /> }
@ -470,16 +387,7 @@ class WalletSettings extends Component {
/>
);
if (rejected) {
return [ closeBtn ];
}
switch (step) {
case 'SENDING':
return done
? [ closeBtn ]
: [ closeBtn, sendingBtn ];
case 'CONFIRMATION':
const { changes } = this.store;

View File

@ -14,24 +14,22 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { noop } from 'lodash';
import { observable, computed, action, transaction } from 'mobx';
import BigNumber from 'bignumber.js';
import { validateUint, validateAddress } from '~/util/validation';
import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
import { ERROR_CODES } from '~/api/transport/error';
const STEPS = {
EDIT: { title: 'wallet settings' },
CONFIRMATION: { title: 'confirmation' },
SENDING: { title: 'sending transaction', waiting: true }
CONFIRMATION: { title: 'confirmation' }
};
export default class WalletSettingsStore {
accounts = {};
onClose = noop;
@observable deployState = '';
@observable done = false;
@observable fromString = false;
@observable requests = [];
@observable step = null;
@ -73,13 +71,6 @@ export default class WalletSettingsStore {
});
}
@computed get waiting () {
this.steps
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
}
@action
changesFromString (json) {
try {
@ -203,7 +194,7 @@ export default class WalletSettingsStore {
});
}
constructor (api, wallet) {
constructor (api, { onClose, wallet }) {
this.api = api;
this.step = this.stepsKeys[0];
@ -223,6 +214,8 @@ export default class WalletSettingsStore {
this.validateWallet(this.wallet);
});
this.onClose = onClose;
}
@action onNext = () => {
@ -267,40 +260,8 @@ export default class WalletSettingsStore {
const changes = this.changes;
const walletInstance = this.walletInstance;
this.step = 'SENDING';
this.onTransactionsState('postTransaction');
Promise
.all(changes.map((change) => this.sendChange(change, walletInstance)))
.then((requestIds) => {
this.onTransactionsState('checkRequest');
this.requests = requestIds.map((id) => ({ id, rejected: false, txhash: null }));
return Promise
.all(requestIds.map((id) => {
return this.api
.pollMethod('parity_checkRequest', id)
.then((txhash) => {
const index = this.requests.findIndex((r) => r.id === id);
this.requests[index].txhash = txhash;
})
.catch((e) => {
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
const index = this.requests.findIndex((r) => r.id === id);
this.requests[index].rejected = true;
return false;
}
throw e;
});
}));
})
.then(() => {
this.done = true;
this.onTransactionsState('completed');
});
Promise.all(changes.map((change) => this.sendChange(change, walletInstance)));
this.onClose();
}
@action sendChange = (change, walletInstance) => {
@ -356,23 +317,6 @@ export default class WalletSettingsStore {
}
}
@action onTransactionsState = (state) => {
switch (state) {
case 'estimateGas':
case 'postTransaction':
this.deployState = 'Preparing transaction for network transmission';
return;
case 'checkRequest':
this.deployState = 'Waiting for confirmation of the transaction in the Parity Secure Signer';
return;
case 'completed':
this.deployState = '';
return;
}
}
@action validateWallet = (_wallet) => {
const senderValidation = validateAddress(_wallet.sender);
const requireValidation = validateUint(_wallet.require);

View File

@ -25,6 +25,7 @@ export blockchainReducer from './blockchainReducer';
export workerReducer from './workerReducer';
export imagesReducer from './imagesReducer';
export personalReducer from './personalReducer';
export requestsReducer from './requestsReducer';
export signerReducer from './signerReducer';
export snackbarReducer from './snackbarReducer';
export statusReducer from './statusReducer';

View File

@ -0,0 +1,171 @@
// Copyright 2015-2017 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 { outTransaction } from '~/api/format/output';
import { trackRequest as trackRequestUtil, parseTransactionReceipt } from '~/util/tx';
import SavedRequests from '~/views/Application/Requests/savedRequests';
const savedRequests = new SavedRequests();
export const init = (api) => (dispatch) => {
api.subscribe('parity_postTransaction', (error, request) => {
if (error) {
return console.error(error);
}
dispatch(watchRequest(request));
});
api.on('connected', () => {
savedRequests.load(api).then((requests) => {
requests.forEach((request) => dispatch(watchRequest(request)));
});
});
};
export const watchRequest = (request) => (dispatch, getState) => {
const { requestId } = request;
// Convert value to BigNumber
request.transaction = outTransaction(request.transaction);
dispatch(setRequest(requestId, request));
dispatch(trackRequest(requestId, request));
};
export const trackRequest = (requestId, { transactionHash = null } = {}) => (dispatch, getState) => {
const { api } = getState();
trackRequestUtil(api, { requestId, transactionHash }, (error, data) => {
if (error) {
console.error(error);
return dispatch(setRequest(requestId, { error }));
}
// Hide the request after 6 mined blocks
if (data.transactionReceipt) {
const { transactionReceipt } = data;
const { requests } = getState();
const requestData = requests[requestId];
let blockSubscriptionId = -1;
// If the request was a contract deployment,
// then add the contract with the saved metadata to the account
if (requestData.metadata && requestData.metadata.deployment) {
const { metadata } = requestData;
const options = {
...requestData.transaction,
metadata
};
parseTransactionReceipt(api, options, data.transactionReceipt)
.then((contractAddress) => {
// No contract address given, might need some confirmations
// from the wallet owners...
if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
return false;
}
metadata.blockNumber = data.transactionReceipt
? data.transactionReceipt.blockNumber.toNumber()
: null;
const prevRequest = getState().requests[requestId];
const nextTransaction = {
...prevRequest.transaction,
creates: contractAddress
};
dispatch(setRequest(requestId, { transaction: nextTransaction }));
return Promise.all([
api.parity.setAccountName(contractAddress, metadata.name),
api.parity.setAccountMeta(contractAddress, metadata)
]);
})
.catch((error) => {
console.error(error);
});
}
api
.subscribe('eth_blockNumber', (error, blockNumber) => {
if (error || !blockNumber) {
return;
}
// Transaction included in `blockHeight` blocks
const blockHeight = blockNumber.minus(transactionReceipt.blockNumber).plus(1);
const nextData = { blockHeight };
// Hide the transaction after 6 blocks
if (blockHeight.gt(6)) {
return dispatch(hideRequest(requestId));
}
return dispatch(setRequest(requestId, nextData, false));
})
.then((subId) => {
blockSubscriptionId = subId;
return dispatch(setRequest(requestId, { blockSubscriptionId }, false));
});
}
return dispatch(setRequest(requestId, data));
});
};
export const hideRequest = (requestId) => (dispatch, getState) => {
const { api, requests } = getState();
const request = requests[requestId];
dispatch(setRequest(requestId, { show: false }));
// Delete it if an error occured or if completed
if (request.error || request.transactionReceipt) {
// Wait for the animation to be done to delete the request
setTimeout(() => {
dispatch(deleteRequest(requestId));
}, 1000);
}
// Unsubscribe to eth-blockNumber if subscribed
if (request.blockSubscriptionId) {
api.unsubscribe(request.blockSubscriptionId);
dispatch(setRequest(requestId, { blockSubscriptionId: null }, false));
}
};
export const setRequest = (requestId, requestData, autoSetShow = true) => {
if (autoSetShow && requestData.show === undefined) {
requestData.show = true;
}
savedRequests.save(requestId, requestData);
return {
type: 'setRequest',
requestId, requestData
};
};
export const deleteRequest = (requestId) => {
savedRequests.remove(requestId);
return {
type: 'deleteRequest',
requestId
};
};

View File

@ -0,0 +1,111 @@
// Copyright 2015-2017 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 { hideRequest, trackRequest, watchRequest } from './requestsActions';
const TX_HASH = '0x123456';
const BASE_REQUEST = {
requestId: '0x1',
transaction: {
from: '0x0',
to: '0x1'
}
};
let api;
let store;
let dispatcher;
function createApi () {
return {
pollMethod: (method, data) => {
switch (method) {
case 'parity_checkRequest':
return Promise.resolve(TX_HASH);
default:
return Promise.resolve();
}
}
};
}
function createRedux (dispatcher) {
return {
dispatch: (arg) => {
if (typeof arg === 'function') {
return arg(store.dispatch, store.getState);
}
return dispatcher(arg);
},
getState: () => {
return {
api,
requests: {
[BASE_REQUEST.requestId]: BASE_REQUEST
}
};
}
};
}
describe('redux/requests', () => {
beforeEach(() => {
api = createApi();
dispatcher = sinon.spy();
store = createRedux(dispatcher);
});
it('watches new requests', () => {
store.dispatch(watchRequest(BASE_REQUEST));
expect(dispatcher).to.be.calledWith({
type: 'setRequest',
requestId: BASE_REQUEST.requestId,
requestData: BASE_REQUEST
});
});
it('tracks requests', (done) => {
store.dispatch(trackRequest(BASE_REQUEST.requestId));
setTimeout(() => {
expect(dispatcher).to.be.calledWith({
type: 'setRequest',
requestId: BASE_REQUEST.requestId,
requestData: {
transactionHash: TX_HASH,
show: true
}
});
done();
}, 50);
});
it('hides requests', () => {
store.dispatch(hideRequest(BASE_REQUEST.requestId));
expect(dispatcher).to.be.calledWith({
type: 'setRequest',
requestId: BASE_REQUEST.requestId,
requestData: { show: false }
});
});
});

View File

@ -0,0 +1,43 @@
// Copyright 2015-2017 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 { handleActions } from 'redux-actions';
const initialState = {};
export default handleActions({
setRequest (state, action) {
const { requestId, requestData } = action;
const nextState = {
...state,
[requestId]: {
...(state[requestId] || {}),
...requestData
}
};
return nextState;
},
deleteRequest (state, action) {
const { requestId } = action;
const nextState = { ...state };
delete nextState[requestId];
return nextState;
}
}, initialState);

View File

@ -121,7 +121,7 @@ export default class Status {
_subscribeBlockNumber = () => {
return this._api
.subscribe('eth_blockNumber', (error, blockNumber) => {
if (error) {
if (error || !blockNumber) {
return;
}

View File

@ -19,7 +19,7 @@ import { routerReducer } from 'react-router-redux';
import {
apiReducer, balancesReducer, blockchainReducer,
workerReducer, imagesReducer, personalReducer,
workerReducer, imagesReducer, personalReducer, requestsReducer,
signerReducer, statusReducer as nodeStatusReducer,
snackbarReducer, walletReducer
} from './providers';
@ -45,6 +45,7 @@ export default function () {
nodeStatus: nodeStatusReducer,
personal: personalReducer,
registry: registryReducer,
requests: requestsReducer,
signer: signerReducer,
snackbar: snackbarReducer,
wallet: walletReducer,

View File

@ -20,6 +20,7 @@ import initMiddleware from './middleware';
import initReducers from './reducers';
import { load as loadWallet } from './providers/walletActions';
import { init as initRequests } from './providers/requestsActions';
import { setupWorker } from './providers/workerWrapper';
import {
@ -44,6 +45,7 @@ export default function (api, browserHistory, forEmbed = false) {
new SignerProvider(store, api).start();
store.dispatch(loadWallet(api));
store.dispatch(initRequests(api));
setupWorker(store);
return store;

View File

@ -49,6 +49,7 @@ export default class SecureApi extends Api {
// When the transport is closed, try to reconnect
transport.on('close', this.connect, this);
this.connect();
}

View File

@ -28,7 +28,7 @@ import styles from './inputAddress.css';
class InputAddress extends Component {
static propTypes = {
accountsInfo: PropTypes.object,
account: PropTypes.object,
allowCopy: PropTypes.bool,
autoFocus: PropTypes.bool,
allowInvalid: PropTypes.bool,
@ -47,7 +47,6 @@ class InputAddress extends Component {
small: PropTypes.bool,
tabIndex: PropTypes.number,
text: PropTypes.bool,
tokens: PropTypes.object,
value: PropTypes.string
};
@ -58,10 +57,9 @@ class InputAddress extends Component {
};
render () {
const { accountsInfo, allowCopy, autoFocus, className, disabled, error, focused, hint } = this.props;
const { account, allowCopy, autoFocus, className, disabled, error, focused, hint } = this.props;
const { hideUnderline, label, onClick, onFocus, readOnly, small } = this.props;
const { tabIndex, text, tokens, value } = this.props;
const account = value && (accountsInfo[value] || tokens[value]);
const { tabIndex, text, value } = this.props;
const icon = this.renderIcon();
const classes = [ className ];
@ -168,13 +166,26 @@ class InputAddress extends Component {
}
}
function mapStateToProps (state) {
const { tokens } = state.balances;
function mapStateToProps (state, props) {
const { text, value } = props;
if (!text || !value) {
return {};
}
const lcValue = value.toLowerCase();
const { accountsInfo } = state.personal;
const { tokens } = state.balances;
const accountsInfoAddress = Object.keys(accountsInfo).find((address) => address.toLowerCase() === lcValue);
const tokensAddress = Object.keys(tokens).find((address) => address.toLowerCase() === lcValue);
const account = (accountsInfoAddress && accountsInfo[accountsInfoAddress]) ||
(tokensAddress && tokens[tokensAddress]) ||
null;
return {
accountsInfo,
tokens
account
};
}

View File

@ -33,14 +33,20 @@ const TOKEN_METHODS = {
class MethodDecoding extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
};
static propTypes = {
address: PropTypes.string.isRequired,
compact: PropTypes.bool,
token: PropTypes.object,
transaction: PropTypes.object,
historic: PropTypes.bool
}
};
static defaultProps = {
compact: false,
historic: false
};
state = {
contractAddress: null,
@ -54,7 +60,7 @@ class MethodDecoding extends Component {
isLoading: true,
expandInput: false,
inputType: 'auto'
}
};
methodDecodingStore = MethodDecodingStore.get(this.context.api);
@ -106,10 +112,10 @@ class MethodDecoding extends Component {
}
renderGas () {
const { historic, transaction } = this.props;
const { compact, historic, transaction } = this.props;
const { gas, gasPrice, value } = transaction;
if (!gas || !gasPrice) {
if (!gas || !gasPrice || compact) {
return null;
}
@ -248,7 +254,12 @@ class MethodDecoding extends Component {
}
renderInputValue () {
const { transaction } = this.props;
const { compact, transaction } = this.props;
if (compact) {
return null;
}
const { expandInput, inputType } = this.state;
const input = transaction.input || transaction.data;
@ -347,7 +358,7 @@ class MethodDecoding extends Component {
}
renderDeploy () {
const { historic, transaction } = this.props;
const { compact, historic, transaction } = this.props;
const { methodInputs } = this.state;
const { value } = transaction;
@ -384,22 +395,22 @@ class MethodDecoding extends Component {
/>
</div>
{ this.renderAddressName(transaction.creates, false) }
<div>
{
methodInputs && methodInputs.length
!compact && methodInputs && methodInputs.length
? (
<div>
<FormattedMessage
id='ui.methodDecoding.deploy.params'
defaultMessage='with the following parameters:'
/>
)
: ''
}
</div>
<div className={ styles.inputs }>
{ this.renderInputs() }
</div>
</div>
)
: null
}
</div>
);
}
@ -474,15 +485,18 @@ class MethodDecoding extends Component {
}
renderSignatureMethod () {
const { historic, transaction } = this.props;
const { compact, historic, transaction } = this.props;
const { methodName, methodInputs } = this.state;
const showInputs = !compact && methodInputs && methodInputs.length > 0;
const showEth = !!(transaction.value && transaction.value.gt(0));
const method = (
<span className={ styles.name }>
{ methodName }
</span>
);
const ethValue = (
const ethValue = showEth && (
<span className={ styles.highlight }>
{ this.renderEtherValue(transaction.value) }
</span>
@ -493,19 +507,27 @@ class MethodDecoding extends Component {
<div className={ styles.description }>
<FormattedMessage
id='ui.methodDecoding.signature.info'
defaultMessage='{historic, select, true {Executed} false {Will execute}} the {method} function on the contract {address} trsansferring {ethValue}{inputLength, plural, zero {,} other {passing the following {inputLength, plural, one {parameter} other {parameters}}}}'
defaultMessage='{historic, select, true {Executed} false {Will execute}} the {method} function on the contract {address} {showEth, select, true {transferring {ethValue}} false {}} {showInputs, select, false {} true {passing the following {inputLength, plural, one {parameter} other {parameters}}}}'
values={ {
historic,
method,
ethValue,
showEth,
showInputs,
address: this.renderAddressName(transaction.to),
inputLength: methodInputs.length
} }
/>
</div>
{
showInputs
? (
<div className={ styles.inputs }>
{ this.renderInputs() }
</div>
)
: null
}
</div>
);
}

View File

@ -14,22 +14,4 @@
// 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 styles from '../deployContract.css';
export default class ErrorStep extends Component {
static propTypes = {
error: PropTypes.object
}
render () {
const { error } = this.props;
return (
<div className={ styles.center }>
The contract deployment failed: { error.message }
</div>
);
}
}
export default from './scrollableText';

View File

@ -0,0 +1,34 @@
/* Copyright 2015-2017 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/>.
*/
.input {
background-color: transparent;
border: none;
box-sizing: border-box;
color: inherit;
cursor: text;
display: inline-block;
font-family: inherit;
font-size: inherit;
margin-left: 0.25em;
padding: 0;
width: 100%;
&.small {
width: 10em;
}
}

View File

@ -0,0 +1,40 @@
// Copyright 2015-2017 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, { PropTypes } from 'react';
import styles from './scrollableText.css';
export default function ScrollableText ({ small = false, text }) {
const classes = [ styles.input ];
if (small) {
classes.push(styles.small);
}
return (
<input
className={ classes.join(' ') }
readOnly
value={ text }
/>
);
}
ScrollableText.propTypes = {
text: PropTypes.string.isRequired,
small: PropTypes.bool
};

View File

@ -36,7 +36,7 @@ export default class ShortenedHash extends Component {
}
return (
<abbr className={ styles.hash } title={ shortened }>{ shortened }</abbr>
<abbr className={ styles.hash } title={ data }>{ shortened }</abbr>
);
}
}

View File

@ -76,7 +76,7 @@ export default class Title extends Component {
renderSteps () {
const { activeStep, steps } = this.props;
if (!steps) {
if (!steps || steps.length < 2) {
return;
}

View File

@ -46,6 +46,7 @@ export Page from './Page';
export ParityBackground from './ParityBackground';
export Portal from './Portal';
export QrCode from './QrCode';
export ScrollableText from './ScrollableText';
export SectionList from './SectionList';
export SelectionList from './SelectionList';
export ShortenedHash from './ShortenedHash';

View File

@ -16,6 +16,25 @@
import WalletsUtils from '~/util/wallets';
export function trackRequest (api, options, statusCallback) {
const { requestId, transactionHash } = options;
const txHashPromise = transactionHash
? Promise.resolve(transactionHash)
: api.pollMethod('parity_checkRequest', requestId);
return txHashPromise
.then((transactionHash) => {
statusCallback(null, { transactionHash });
return api.pollMethod('eth_getTransactionReceipt', transactionHash, isValidReceipt);
})
.then((transactionReceipt) => {
statusCallback(null, { transactionReceipt });
})
.catch((error) => {
statusCallback(error);
});
}
const isValidReceipt = (receipt) => {
return receipt && receipt.blockNumber && receipt.blockNumber.gt(0);
};
@ -73,100 +92,6 @@ export function postTransaction (_func, _options, _values = []) {
});
}
export function deploy (contract, _options, values, metadata = {}, statecb = () => {}, skipGasEstimate = false) {
const options = { ..._options };
const { api } = contract;
const address = options.from;
return WalletsUtils
.isWallet(api, address)
.then((isWallet) => {
if (!isWallet) {
return contract.deploy(options, values, statecb, skipGasEstimate);
}
let gasEstPromise;
if (skipGasEstimate) {
gasEstPromise = Promise.resolve(null);
} else {
statecb(null, { state: 'estimateGas' });
gasEstPromise = deployEstimateGas(contract, options, values)
.then(([gasEst, gas]) => gas);
}
return gasEstPromise
.then((gas) => {
if (gas) {
options.gas = gas.toFixed(0);
}
statecb(null, { state: 'postTransaction', gas: options.gas });
return WalletsUtils.getDeployArgs(contract, options, values);
})
.then((callArgs) => {
const { func, options, values } = callArgs;
return func._postTransaction(options, values)
.then((requestId) => {
statecb(null, { state: 'checkRequest', requestId });
return contract._pollCheckRequest(requestId);
})
.then((txhash) => {
statecb(null, { state: 'getTransactionReceipt', txhash });
return contract._pollTransactionReceipt(txhash, options.gas);
})
.then((receipt) => {
if (receipt.gasUsed.eq(options.gas)) {
throw new Error(`Contract not deployed, gasUsed == ${options.gas.toFixed(0)}`);
}
const logs = WalletsUtils.parseLogs(api, receipt.logs || []);
const confirmationLog = logs.find((log) => log.event === 'ConfirmationNeeded');
const transactionLog = logs.find((log) => log.event === 'SingleTransact');
if (!confirmationLog && !transactionLog) {
throw new Error('Something went wrong in the Wallet Contract (no logs have been emitted)...');
}
// Confirmations are needed from the other owners
if (confirmationLog) {
const operationHash = api.util.bytesToHex(confirmationLog.params.operation.value);
// Add the contract to pending contracts
WalletsUtils.addPendingContract(address, operationHash, metadata);
statecb(null, { state: 'confirmationNeeded' });
return;
}
// Set the contract address in the receip
receipt.contractAddress = transactionLog.params.created.value;
const contractAddress = receipt.contractAddress;
statecb(null, { state: 'hasReceipt', receipt });
contract._receipt = receipt;
contract._address = contractAddress;
statecb(null, { state: 'getCode' });
return api.eth.getCode(contractAddress)
.then((code) => {
if (code === '0x') {
throw new Error('Contract not deployed, getCode returned 0x');
}
statecb(null, { state: 'completed' });
return contractAddress;
});
});
});
});
}
export function deployEstimateGas (contract, _options, values) {
const options = { ..._options };
const { api } = contract;
@ -192,6 +117,86 @@ export function deployEstimateGas (contract, _options, values) {
});
}
export function deploy (contract, options, values, skipGasEstimate = false) {
const { api } = contract;
const address = options.from;
const gasEstPromise = skipGasEstimate
? Promise.resolve(null)
: deployEstimateGas(contract, options, values).then(([gasEst, gas]) => gas);
return gasEstPromise
.then((gas) => {
if (gas) {
options.gas = gas.toFixed(0);
}
return WalletsUtils.isWallet(api, address);
})
.then((isWallet) => {
if (!isWallet) {
const encodedOptions = contract._encodeOptions(contract.constructors[0], options, values);
return api.parity.postTransaction(encodedOptions);
}
return WalletsUtils.getDeployArgs(contract, options, values)
.then((callArgs) => {
const { func, options, values } = callArgs;
return func._postTransaction(options, values);
});
});
}
export function parseTransactionReceipt (api, options, receipt) {
const { metadata } = options;
const address = options.from;
if (receipt.gasUsed.eq(options.gas)) {
const error = new Error(`Contract not deployed, gasUsed == ${options.gas.toFixed(0)}`);
return Promise.reject(error);
}
const logs = WalletsUtils.parseLogs(api, receipt.logs || []);
const confirmationLog = logs.find((log) => log.event === 'ConfirmationNeeded');
const transactionLog = logs.find((log) => log.event === 'SingleTransact');
if (!confirmationLog && !transactionLog && !receipt.contractAddress) {
const error = new Error('Something went wrong in the contract deployment...');
return Promise.reject(error);
}
// Confirmations are needed from the other owners
if (confirmationLog) {
const operationHash = api.util.bytesToHex(confirmationLog.params.operation.value);
// Add the contract to pending contracts
WalletsUtils.addPendingContract(address, operationHash, metadata);
return Promise.resolve(null);
}
if (transactionLog) {
// Set the contract address in the receipt
receipt.contractAddress = transactionLog.params.created.value;
}
const contractAddress = receipt.contractAddress;
return api.eth
.getCode(contractAddress)
.then((code) => {
if (code === '0x') {
throw new Error('Contract not deployed, getCode returned 0x');
}
return contractAddress;
});
}
export function patchApi (api) {
api.patch = {
...api.patch,

View File

@ -25,6 +25,7 @@ import { NULL_ADDRESS } from './constants';
export const ERRORS = {
invalidAddress: 'address is an invalid network address',
invalidAmount: 'the supplied amount should be a valid positive number',
invalidAmountDecimals: 'the supplied amount exceeds the allowed decimals',
duplicateAddress: 'the address is already in your address book',
invalidChecksum: 'address has failed the checksum formatting',
invalidName: 'name should not be blank and longer than 2',
@ -48,6 +49,7 @@ export function validateAbi (abi) {
abiError = ERRORS.invalidAbi;
return {
error: abiError,
abi,
abiError,
abiParsed
@ -66,6 +68,7 @@ export function validateAbi (abi) {
abiError = `${ERRORS.invalidAbi} (#${invalidIndex}: ${invalid.name || invalid.type})`;
return {
error: abiError,
abi,
abiError,
abiParsed
@ -78,6 +81,7 @@ export function validateAbi (abi) {
}
return {
error: abiError,
abi,
abiError,
abiParsed
@ -123,6 +127,7 @@ export function validateAddress (address) {
}
return {
error: addressError,
address,
addressError
};
@ -138,6 +143,7 @@ export function validateCode (code) {
}
return {
error: codeError,
code,
codeError
};
@ -149,6 +155,7 @@ export function validateName (name) {
: null;
return {
error: nameError,
name,
nameError
};
@ -168,6 +175,27 @@ export function validatePositiveNumber (number) {
}
return {
error: numberError,
number,
numberError
};
}
export function validateDecimalsNumber (number, base = 1) {
let numberError = null;
try {
const s = new BigNumber(number).mul(base).toFixed();
if (s.indexOf('.') !== -1) {
numberError = ERRORS.invalidAmountDecimals;
}
} catch (e) {
numberError = ERRORS.invalidAmount;
}
return {
error: numberError,
number,
numberError
};
@ -189,6 +217,7 @@ export function validateUint (value) {
}
return {
error: valueError,
value,
valueError
};

View File

@ -32,7 +32,8 @@ describe('util/validation', () => {
name: 'test',
inputs: [],
outputs: []
}]
}],
error: null
});
});
@ -47,7 +48,8 @@ describe('util/validation', () => {
name: 'test',
inputs: [],
outputs: []
}]
}],
error: null
});
});
@ -57,7 +59,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: ERRORS.invalidAbi,
abiParsed: null
abiParsed: null,
error: ERRORS.invalidAbi
});
});
@ -67,7 +70,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: ERRORS.invalidAbi,
abiParsed: {}
abiParsed: {},
error: ERRORS.invalidAbi
});
});
@ -77,7 +81,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: `${ERRORS.invalidAbi} (#0: event)`,
abiParsed: [{ type: 'event' }]
abiParsed: [{ type: 'event' }],
error: `${ERRORS.invalidAbi} (#0: event)`
});
});
@ -87,7 +92,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: `${ERRORS.invalidAbi} (#0: function)`,
abiParsed: [{ type: 'function' }]
abiParsed: [{ type: 'function' }],
error: `${ERRORS.invalidAbi} (#0: function)`
});
});
@ -97,7 +103,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({
abi,
abiError: `${ERRORS.invalidAbi} (#0: somethingElse)`,
abiParsed: [{ type: 'somethingElse' }]
abiParsed: [{ type: 'somethingElse' }],
error: `${ERRORS.invalidAbi} (#0: somethingElse)`
});
});
});
@ -108,7 +115,8 @@ describe('util/validation', () => {
expect(validateAddress(address)).to.deep.equal({
address,
addressError: null
addressError: null,
error: null
});
});
@ -117,14 +125,16 @@ describe('util/validation', () => {
expect(validateAddress(address.toLowerCase())).to.deep.equal({
address,
addressError: null
addressError: null,
error: null
});
});
it('sets error on null addresses', () => {
expect(validateAddress(null)).to.deep.equal({
address: null,
addressError: ERRORS.invalidAddress
addressError: ERRORS.invalidAddress,
error: ERRORS.invalidAddress
});
});
@ -133,7 +143,8 @@ describe('util/validation', () => {
expect(validateAddress(address)).to.deep.equal({
address,
addressError: ERRORS.invalidAddress
addressError: ERRORS.invalidAddress,
error: ERRORS.invalidAddress
});
});
});
@ -142,35 +153,40 @@ describe('util/validation', () => {
it('validates hex code', () => {
expect(validateCode('0x123abc')).to.deep.equal({
code: '0x123abc',
codeError: null
codeError: null,
error: null
});
});
it('validates hex code (non-prefix)', () => {
expect(validateCode('123abc')).to.deep.equal({
code: '123abc',
codeError: null
codeError: null,
error: null
});
});
it('sets error on invalid code', () => {
expect(validateCode(null)).to.deep.equal({
code: null,
codeError: ERRORS.invalidCode
codeError: ERRORS.invalidCode,
error: ERRORS.invalidCode
});
});
it('sets error on empty code', () => {
expect(validateCode('')).to.deep.equal({
code: '',
codeError: ERRORS.invalidCode
codeError: ERRORS.invalidCode,
error: ERRORS.invalidCode
});
});
it('sets error on non-hex code', () => {
expect(validateCode('123hfg')).to.deep.equal({
code: '123hfg',
codeError: ERRORS.invalidCode
codeError: ERRORS.invalidCode,
error: ERRORS.invalidCode
});
});
});
@ -179,21 +195,24 @@ describe('util/validation', () => {
it('validates names', () => {
expect(validateName('Joe Bloggs')).to.deep.equal({
name: 'Joe Bloggs',
nameError: null
nameError: null,
error: null
});
});
it('sets error on null names', () => {
expect(validateName(null)).to.deep.equal({
name: null,
nameError: ERRORS.invalidName
nameError: ERRORS.invalidName,
error: ERRORS.invalidName
});
});
it('sets error on short names', () => {
expect(validateName(' 1 ')).to.deep.equal({
name: ' 1 ',
nameError: ERRORS.invalidName
nameError: ERRORS.invalidName,
error: ERRORS.invalidName
});
});
});
@ -202,35 +221,40 @@ describe('util/validation', () => {
it('validates numbers', () => {
expect(validatePositiveNumber(123)).to.deep.equal({
number: 123,
numberError: null
numberError: null,
error: null
});
});
it('validates strings', () => {
expect(validatePositiveNumber('123')).to.deep.equal({
number: '123',
numberError: null
numberError: null,
error: null
});
});
it('validates bignumbers', () => {
expect(validatePositiveNumber(new BigNumber(123))).to.deep.equal({
number: new BigNumber(123),
numberError: null
numberError: null,
error: null
});
});
it('sets error on invalid numbers', () => {
expect(validatePositiveNumber(null)).to.deep.equal({
number: null,
numberError: ERRORS.invalidAmount
numberError: ERRORS.invalidAmount,
error: ERRORS.invalidAmount
});
});
it('sets error on negative numbers', () => {
expect(validatePositiveNumber(-1)).to.deep.equal({
number: -1,
numberError: ERRORS.invalidAmount
numberError: ERRORS.invalidAmount,
error: ERRORS.invalidAmount
});
});
});
@ -239,42 +263,48 @@ describe('util/validation', () => {
it('validates numbers', () => {
expect(validateUint(123)).to.deep.equal({
value: 123,
valueError: null
valueError: null,
error: null
});
});
it('validates strings', () => {
expect(validateUint('123')).to.deep.equal({
value: '123',
valueError: null
valueError: null,
error: null
});
});
it('validates bignumbers', () => {
expect(validateUint(new BigNumber(123))).to.deep.equal({
value: new BigNumber(123),
valueError: null
valueError: null,
error: null
});
});
it('sets error on invalid numbers', () => {
expect(validateUint(null)).to.deep.equal({
value: null,
valueError: ERRORS.invalidNumber
valueError: ERRORS.invalidNumber,
error: ERRORS.invalidNumber
});
});
it('sets error on negative numbers', () => {
expect(validateUint(-1)).to.deep.equal({
value: -1,
valueError: ERRORS.negativeNumber
valueError: ERRORS.negativeNumber,
error: ERRORS.negativeNumber
});
});
it('sets error on decimal numbers', () => {
expect(validateUint(3.1415927)).to.deep.equal({
value: 3.1415927,
valueError: ERRORS.decimalNumber
valueError: ERRORS.decimalNumber,
error: ERRORS.decimalNumber
});
});
});

View File

@ -14,4 +14,4 @@
// 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 './errorStep';
export default from './requests';

View File

@ -0,0 +1,127 @@
/* Copyright 2015-2017 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/>.
*/
$baseColor: 255;
$baseOpacity: 0.95;
.requests {
align-items: flex-end;
bottom: 2em;
display: flex;
flex-direction: column;
position: fixed;
right: 0.175em;
z-index: 750;
* {
font-size: 0.85rem !important;
}
}
.request {
animation-fill-mode: forwards;
animation-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
background-color: rgba($baseColor, $baseColor, $baseColor, $baseOpacity);
color: black;
cursor: pointer;
margin-top: 0.5em;
opacity: 1;
&.hide {
animation-duration: 0.5s;
animation-name: fadeout;
}
.status {
padding: 0.5em;
&.error {
background-color: rgba(200, 40, 40, 0.95);
color: white;
}
}
.container {
display: flex;
flex-direction: row;
padding: 1em;
* {
color: black !important;
}
}
&:hover .container {
background-color: rgba($baseColor, $baseColor, $baseColor, 1);
}
p {
margin: 0;
}
}
@keyframes fadeout {
from {
display: block;
height: inherit;
opacity: 1;
}
49% {
display: block;
height: inherit;
opacity: 0;
}
98% {
display: block;
height: 0;
opacity: 0;
}
to {
display: none;
height: 0;
opacity: 0;
}
}
.identity {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
margin-right: 1em;
.icon {
margin-bottom: 0.5rem;
}
}
.inline {
align-items: center;
display: flex;
flex-direction: row;
.fill {
flex: 1 0 auto;
}
}
.hash {
margin-left: 0.25em;
}

View File

@ -0,0 +1,233 @@
// Copyright 2015-2017 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 { LinearProgress } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { hideRequest } from '~/redux/providers/requestsActions';
import { MethodDecoding, IdentityIcon, ScrollableText, ShortenedHash } from '~/ui';
import styles from './requests.css';
const ERROR_STATE = 'ERROR_STATE';
const DONE_STATE = 'DONE_STATE';
const WAITING_STATE = 'WAITING_STATE';
class Requests extends Component {
static propTypes = {
requests: PropTypes.object.isRequired,
onHideRequest: PropTypes.func.isRequired
};
state = {
extras: {}
};
render () {
const { requests } = this.props;
const { extras } = this.state;
return (
<div className={ styles.requests }>
{ Object.values(requests).map((request) => this.renderRequest(request, extras[request.requestId])) }
</div>
);
}
renderRequest (request, extras = {}) {
const { show, transaction } = request;
const state = this.getTransactionState(request);
const displayedTransaction = { ...transaction };
// Don't show gas and gasPrice
delete displayedTransaction.gas;
delete displayedTransaction.gasPrice;
const requestClasses = [ styles.request ];
const statusClasses = [ styles.status ];
const requestStyle = {};
const handleHideRequest = () => {
this.handleHideRequest(request.requestId);
};
if (state.type === ERROR_STATE) {
statusClasses.push(styles.error);
}
if (!show) {
requestClasses.push(styles.hide);
}
// Set the Request height (for animation) if found
if (extras.height) {
requestStyle.height = extras.height;
}
return (
<div
className={ requestClasses.join(' ') }
key={ request.requestId }
ref={ `request_${request.requestId}` }
onClick={ handleHideRequest }
style={ requestStyle }
>
<div className={ statusClasses.join(' ') }>
{ this.renderStatus(request) }
</div>
{
state.type === ERROR_STATE
? null
: (
<LinearProgress
max={ 6 }
mode={ state.type === WAITING_STATE ? 'indeterminate' : 'determinate' }
value={ state.type === DONE_STATE ? request.blockHeight.toNumber() : 6 }
/>
)
}
<div className={ styles.container }>
<div className={ styles.identity } title={ transaction.from }>
<IdentityIcon
address={ transaction.from }
inline
center
className={ styles.icon }
/>
</div>
<MethodDecoding
address={ transaction.from }
compact
historic={ state.type === DONE_STATE }
transaction={ displayedTransaction }
/>
</div>
</div>
);
}
renderStatus (request) {
const { error, transactionHash, transactionReceipt } = request;
if (error) {
return (
<div
className={ styles.inline }
title={ error.message }
>
<FormattedMessage
id='requests.status.error'
defaultMessage='An error occured:'
/>
<div className={ styles.fill }>
<ScrollableText
text={ error.text || error.message }
/>
</div>
</div>
);
}
if (transactionReceipt) {
return (
<FormattedMessage
id='requests.status.transactionMined'
defaultMessage='Transaction mined at block #{blockNumber} ({blockHeight} blocks ago)'
values={ {
blockHeight: request.blockHeight.toNumber(),
blockNumber: transactionReceipt.blockNumber.toFormat()
} }
/>
);
}
if (transactionHash) {
return (
<div className={ styles.inline }>
<FormattedMessage
id='requests.status.transactionSent'
defaultMessage='Transaction sent to network with hash'
/>
<div className={ [ styles.fill, styles.hash ].join(' ') }>
<ShortenedHash data={ transactionHash } />
</div>
</div>
);
}
return (
<FormattedMessage
id='requests.status.waitingForSigner'
defaultMessage='Waiting for authorization in the Parity Signer'
/>
);
}
getTransactionState (request) {
const { error, transactionReceipt } = request;
if (error) {
return { type: ERROR_STATE };
}
if (transactionReceipt) {
return { type: DONE_STATE };
}
return { type: WAITING_STATE };
}
handleHideRequest = (requestId) => {
const requestElement = ReactDOM.findDOMNode(this.refs[`request_${requestId}`]);
// Try to get the request element height, to have a nice transition effect
if (requestElement) {
const { height } = requestElement.getBoundingClientRect();
const prevExtras = this.state.extras;
const nextExtras = {
...prevExtras,
[ requestId ]: {
...prevExtras[requestId],
height
}
};
return this.setState({ extras: nextExtras }, () => {
return this.props.onHideRequest(requestId);
});
}
return this.props.onHideRequest(requestId);
}
}
const mapStateToProps = (state) => {
const { requests } = state;
return { requests };
};
function mapDispatchToProps (dispatch) {
return bindActionCreators({
onHideRequest: hideRequest
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(Requests);

View File

@ -0,0 +1,89 @@
// Copyright 2015-2017 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 store from 'store';
import { ERROR_CODES } from '~/api/transport/error';
export const LS_REQUESTS_KEY = '_parity::requests';
export default class SavedRequests {
load (api) {
const requests = this._get();
const promises = Object.values(requests).map((request) => {
const { requestId, transactionHash } = request;
// The request hasn't been signed yet
if (transactionHash) {
return request;
}
return this._requestExists(api, requestId)
.then((exists) => {
if (!exists) {
return null;
}
return request;
})
.catch(() => {
this.remove(requestId);
});
});
return Promise.all(promises).then((requests) => requests.filter((request) => request));
}
save (requestId, requestData) {
const requests = this._get();
requests[requestId] = {
...(requests[requestId] || {}),
...requestData
};
this._set(requests);
}
remove (requestId) {
const requests = this._get();
delete requests[requestId];
this._set(requests);
}
_get () {
return store.get(LS_REQUESTS_KEY) || {};
}
_set (requests = {}) {
return store.set(LS_REQUESTS_KEY, requests);
}
_requestExists (api, requestId) {
return api.parity
.checkRequest(requestId)
.then(() => true)
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_NOT_FOUND) {
return false;
}
throw error;
});
}
}

View File

@ -0,0 +1,88 @@
// Copyright 2015-2017 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 from 'store';
import SavedRequests, { LS_REQUESTS_KEY } from './savedRequests';
const DEFAULT_REQUEST = {
requestId: '0x1',
transaction: {}
};
const api = createApi();
const savedRequests = new SavedRequests();
function createApi () {
return {
parity: {
checkRequest: sinon.stub().resolves()
}
};
}
describe('views/Application/Requests/savedRequests', () => {
beforeEach(() => {
store.set(LS_REQUESTS_KEY, {
[DEFAULT_REQUEST.requestId]: DEFAULT_REQUEST
});
});
afterEach(() => {
store.set(LS_REQUESTS_KEY, {});
});
it('gets requests from local storage', () => {
const requests = savedRequests._get();
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal(DEFAULT_REQUEST);
});
it('sets requests to local storage', () => {
savedRequests._set({});
const requests = savedRequests._get();
expect(requests).to.deep.equal({});
});
it('removes requests', () => {
savedRequests.remove(DEFAULT_REQUEST.requestId);
const requests = savedRequests._get();
expect(requests).to.deep.equal({});
});
it('saves new requests', () => {
savedRequests.save(DEFAULT_REQUEST.requestId, { extraData: true });
const requests = savedRequests._get();
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal({
...DEFAULT_REQUEST,
extraData: true
});
});
it('loads requests', () => {
return savedRequests.load(api)
.then((requests) => {
expect(requests[0]).to.deep.equal(DEFAULT_REQUEST);
});
});
});

View File

@ -31,6 +31,7 @@ import FrameError from './FrameError';
import Status from './Status';
import Store from './store';
import TabBar from './TabBar';
import Requests from './Requests';
import styles from './application.css';
@ -103,6 +104,7 @@ class Application extends Component {
}
<Extension />
<Snackbar />
<Requests />
</Container>
);
}