Merge branch 'master' into on-demand-les-request

This commit is contained in:
Robert Habermeier 2017-01-04 14:33:45 +01:00
commit 62bc92ff4d
99 changed files with 2837 additions and 1198 deletions

2
Cargo.lock generated
View File

@ -1503,7 +1503,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#d95a7dd2cc7469dc58af77743ec3ebc65e51cf36"
source = "git+https://github.com/ethcore/js-precompiled.git#ebea2bf78e076916b51b04d8b24187a6a85ae440"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

View File

@ -378,7 +378,8 @@ impl BlockProvider for BlockChain {
.enumerate()
.flat_map(move |(index, (mut logs, tx_hash))| {
let current_log_index = log_index;
log_index -= logs.len();
let no_of_logs = logs.len();
log_index -= no_of_logs;
logs.reverse();
logs.into_iter()
@ -390,6 +391,7 @@ impl BlockProvider for BlockChain {
transaction_hash: tx_hash,
// iterating in reverse order
transaction_index: receipts_len - index - 1,
transaction_log_index: no_of_logs - i - 1,
log_index: current_log_index - i - 1,
})
})
@ -1936,6 +1938,7 @@ mod tests {
block_number: block1.header().number(),
transaction_hash: tx_hash1.clone(),
transaction_index: 0,
transaction_log_index: 0,
log_index: 0,
},
LocalizedLogEntry {
@ -1944,6 +1947,7 @@ mod tests {
block_number: block1.header().number(),
transaction_hash: tx_hash1.clone(),
transaction_index: 0,
transaction_log_index: 1,
log_index: 1,
},
LocalizedLogEntry {
@ -1952,6 +1956,7 @@ mod tests {
block_number: block1.header().number(),
transaction_hash: tx_hash2.clone(),
transaction_index: 1,
transaction_log_index: 0,
log_index: 2,
},
LocalizedLogEntry {
@ -1960,6 +1965,7 @@ mod tests {
block_number: block2.header().number(),
transaction_hash: tx_hash3.clone(),
transaction_index: 0,
transaction_log_index: 0,
log_index: 0,
}
]);
@ -1970,6 +1976,7 @@ mod tests {
block_number: block2.header().number(),
transaction_hash: tx_hash3.clone(),
transaction_index: 0,
transaction_log_index: 0,
log_index: 0,
}
]);

View File

@ -59,7 +59,7 @@ use client::{
use client::Error as ClientError;
use env_info::EnvInfo;
use executive::{Executive, Executed, TransactOptions, contract_address};
use receipt::LocalizedReceipt;
use receipt::{Receipt, LocalizedReceipt};
use trace::{TraceDB, ImportRequest as TraceImportRequest, LocalizedTrace, Database as TraceDatabase};
use trace;
use trace::FlatTransactionTraces;
@ -837,7 +837,6 @@ impl snapshot::DatabaseRestore for Client {
}
}
impl BlockChainClient for Client {
fn call(&self, t: &SignedTransaction, block: BlockId, analytics: CallAnalytics) -> Result<Executed, CallError> {
let header = self.block_header(block).ok_or(CallError::StatePruned)?;
@ -1134,53 +1133,23 @@ impl BlockChainClient for Client {
let chain = self.chain.read();
self.transaction_address(id)
.and_then(|address| chain.block_number(&address.block_hash).and_then(|block_number| {
let t = chain.block_body(&address.block_hash)
.and_then(|body| {
body.view().localized_transaction_at(&address.block_hash, block_number, address.index)
});
let transaction = chain.block_body(&address.block_hash)
.and_then(|body| body.view().localized_transaction_at(&address.block_hash, block_number, address.index));
let tx_and_sender = t.and_then(|tx| tx.sender().ok().map(|sender| (tx, sender)));
match (tx_and_sender, chain.transaction_receipt(&address)) {
(Some((tx, sender)), Some(receipt)) => {
let block_hash = tx.block_hash.clone();
let block_number = tx.block_number.clone();
let transaction_hash = tx.hash();
let transaction_index = tx.transaction_index;
let prior_gas_used = match tx.transaction_index {
0 => U256::zero(),
i => {
let prior_address = TransactionAddress { block_hash: address.block_hash, index: i - 1 };
let prior_receipt = chain.transaction_receipt(&prior_address).expect("Transaction receipt at `address` exists; `prior_address` has lower index in same block; qed");
prior_receipt.gas_used
}
};
Some(LocalizedReceipt {
transaction_hash: tx.hash(),
transaction_index: tx.transaction_index,
block_hash: tx.block_hash,
block_number: tx.block_number,
cumulative_gas_used: receipt.gas_used,
gas_used: receipt.gas_used - prior_gas_used,
contract_address: match tx.action {
Action::Call(_) => None,
Action::Create => Some(contract_address(&sender, &tx.nonce))
},
logs: receipt.logs.into_iter().enumerate().map(|(i, log)| LocalizedLogEntry {
entry: log,
block_hash: block_hash.clone(),
block_number: block_number,
transaction_hash: transaction_hash.clone(),
transaction_index: transaction_index,
log_index: i
}).collect(),
log_bloom: receipt.log_bloom,
state_root: receipt.state_root,
let previous_receipts = (0..address.index + 1)
.map(|index| {
let mut address = address.clone();
address.index = index;
chain.transaction_receipt(&address)
})
},
_ => None
}
}))
.collect();
match (transaction, previous_receipts) {
(Some(transaction), Some(previous_receipts)) => {
Some(transaction_receipt(transaction, previous_receipts))
},
_ => None,
}
}))
}
fn tree_route(&self, from: &H256, to: &H256) -> Option<TreeRoute> {
@ -1535,6 +1504,49 @@ impl Drop for Client {
}
}
/// Returns `LocalizedReceipt` given `LocalizedTransaction`
/// and a vector of receipts from given block up to transaction index.
fn transaction_receipt(tx: LocalizedTransaction, mut receipts: Vec<Receipt>) -> LocalizedReceipt {
assert_eq!(receipts.len(), tx.transaction_index + 1, "All previous receipts are provided.");
let sender = tx.sender()
.expect("LocalizedTransaction is part of the blockchain; We have only valid transactions in chain; qed");
let receipt = receipts.pop().expect("Current receipt is provided; qed");
let prior_gas_used = match tx.transaction_index {
0 => 0.into(),
i => receipts.get(i - 1).expect("All previous receipts are provided; qed").gas_used,
};
let no_of_logs = receipts.into_iter().map(|receipt| receipt.logs.len()).sum::<usize>();
let transaction_hash = tx.hash();
let block_hash = tx.block_hash;
let block_number = tx.block_number;
let transaction_index = tx.transaction_index;
LocalizedReceipt {
transaction_hash: transaction_hash,
transaction_index: transaction_index,
block_hash: block_hash,
block_number:block_number,
cumulative_gas_used: receipt.gas_used,
gas_used: receipt.gas_used - prior_gas_used,
contract_address: match tx.action {
Action::Call(_) => None,
Action::Create => Some(contract_address(&sender, &tx.nonce))
},
logs: receipt.logs.into_iter().enumerate().map(|(i, log)| LocalizedLogEntry {
entry: log,
block_hash: block_hash,
block_number: block_number,
transaction_hash: transaction_hash,
transaction_index: transaction_index,
transaction_log_index: i,
log_index: no_of_logs + i,
}).collect(),
log_bloom: receipt.log_bloom,
state_root: receipt.state_root,
}
}
#[cfg(test)]
mod tests {
@ -1570,4 +1582,91 @@ mod tests {
assert!(client.tree_route(&genesis, &new_hash).is_none());
}
#[test]
fn should_return_correct_log_index() {
use super::transaction_receipt;
use ethkey::KeyPair;
use log_entry::{LogEntry, LocalizedLogEntry};
use receipt::{Receipt, LocalizedReceipt};
use transaction::{Transaction, LocalizedTransaction, Action};
use util::Hashable;
// given
let key = KeyPair::from_secret("test".sha3()).unwrap();
let secret = key.secret();
let block_number = 1;
let block_hash = 5.into();
let state_root = 99.into();
let gas_used = 10.into();
let raw_tx = Transaction {
nonce: 0.into(),
gas_price: 0.into(),
gas: 21000.into(),
action: Action::Call(10.into()),
value: 0.into(),
data: vec![],
};
let tx1 = raw_tx.clone().sign(secret, None);
let transaction = LocalizedTransaction {
signed: tx1.clone(),
block_number: block_number,
block_hash: block_hash,
transaction_index: 1,
};
let logs = vec![LogEntry {
address: 5.into(),
topics: vec![],
data: vec![],
}, LogEntry {
address: 15.into(),
topics: vec![],
data: vec![],
}];
let receipts = vec![Receipt {
state_root: state_root,
gas_used: 5.into(),
log_bloom: Default::default(),
logs: vec![logs[0].clone()],
}, Receipt {
state_root: state_root,
gas_used: gas_used,
log_bloom: Default::default(),
logs: logs.clone(),
}];
// when
let receipt = transaction_receipt(transaction, receipts);
// then
assert_eq!(receipt, LocalizedReceipt {
transaction_hash: tx1.hash(),
transaction_index: 1,
block_hash: block_hash,
block_number: block_number,
cumulative_gas_used: gas_used,
gas_used: gas_used - 5.into(),
contract_address: None,
logs: vec![LocalizedLogEntry {
entry: logs[0].clone(),
block_hash: block_hash,
block_number: block_number,
transaction_hash: tx1.hash(),
transaction_index: 1,
transaction_log_index: 0,
log_index: 1,
}, LocalizedLogEntry {
entry: logs[1].clone(),
block_hash: block_hash,
block_number: block_number,
transaction_hash: tx1.hash(),
transaction_index: 1,
transaction_log_index: 1,
log_index: 2,
}],
log_bloom: Default::default(),
state_root: state_root,
});
}
}

View File

@ -320,4 +320,3 @@ fn does_not_propagate_delayed_transactions() {
assert_eq!(2, client.ready_transactions().len());
assert_eq!(2, client.miner().pending_transactions().len());
}

View File

@ -97,6 +97,8 @@ pub struct LocalizedLogEntry {
pub transaction_index: usize,
/// Log position in the block.
pub log_index: usize,
/// Log position in the transaction.
pub transaction_log_index: usize,
}
impl Deref for LocalizedLogEntry {

View File

@ -373,7 +373,7 @@ impl SignedTransaction {
}
/// Signed Transaction that is a part of canon blockchain.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", binary)]
pub struct LocalizedTransaction {
/// Signed part.

View File

@ -1,6 +1,6 @@
{
"name": "parity.js",
"version": "0.2.152",
"version": "0.2.165",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>",
@ -26,9 +26,9 @@
],
"scripts": {
"build": "npm run build:lib && npm run build:dll && npm run build:app",
"build:app": "webpack --config webpack/app --progress",
"build:lib": "webpack --config webpack/libraries --progress",
"build:dll": "webpack --config webpack/vendor --progress",
"build:app": "webpack --config webpack/app",
"build:lib": "webpack --config webpack/libraries",
"build:dll": "webpack --config webpack/vendor",
"ci:build": "npm run ci:build:lib && npm run ci:build:dll && npm run ci:build:app",
"ci:build:app": "NODE_ENV=production webpack --config webpack/app",
"ci:build:lib": "NODE_ENV=production webpack --config webpack/libraries",
@ -51,19 +51,19 @@
},
"devDependencies": {
"babel-cli": "6.18.0",
"babel-core": "6.20.0",
"babel-core": "6.21.0",
"babel-eslint": "7.1.1",
"babel-loader": "6.2.10",
"babel-plugin-lodash": "3.2.10",
"babel-plugin-lodash": "3.2.11",
"babel-plugin-react-intl": "2.2.0",
"babel-plugin-transform-class-properties": "6.18.0",
"babel-plugin-transform-class-properties": "6.19.0",
"babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-plugin-transform-object-rest-spread": "6.20.2",
"babel-plugin-transform-react-remove-prop-types": "0.2.11",
"babel-plugin-transform-runtime": "6.15.0",
"babel-plugin-webpack-alias": "2.1.2",
"babel-polyfill": "6.20.0",
"babel-preset-env": "1.0.2",
"babel-preset-env": "1.1.4",
"babel-preset-es2015": "6.18.0",
"babel-preset-es2016": "6.16.0",
"babel-preset-es2017": "6.16.0",
@ -80,57 +80,58 @@
"coveralls": "2.11.15",
"css-loader": "0.26.1",
"ejs-loader": "0.3.0",
"enzyme": "2.6.0",
"enzyme": "2.7.0",
"eslint": "3.11.1",
"eslint-config-semistandard": "7.0.0",
"eslint-config-standard": "6.2.1",
"eslint-config-standard-react": "4.2.0",
"eslint-plugin-promise": "3.4.0",
"eslint-plugin-react": "6.7.1",
"eslint-plugin-react": "6.8.0",
"eslint-plugin-standard": "2.0.1",
"express": "4.14.0",
"extract-loader": "0.1.0",
"extract-text-webpack-plugin": "2.0.0-beta.4",
"file-loader": "0.9.0",
"happypack": "3.0.0",
"happypack": "3.0.2",
"html-loader": "0.4.4",
"html-webpack-plugin": "2.24.1",
"http-proxy-middleware": "0.17.2",
"http-proxy-middleware": "0.17.3",
"husky": "0.11.9",
"ignore-styles": "5.0.1",
"image-webpack-loader": "3.0.0",
"image-webpack-loader": "3.1.0",
"istanbul": "1.0.0-alpha.2",
"jsdom": "9.8.3",
"jsdom": "9.9.1",
"json-loader": "0.5.4",
"mocha": "3.2.0",
"mock-local-storage": "1.0.2",
"mock-socket": "6.0.3",
"mock-socket": "6.0.4",
"nock": "9.0.2",
"postcss-import": "9.0.0",
"postcss-loader": "1.2.0",
"postcss-loader": "1.2.1",
"postcss-nested": "1.0.0",
"postcss-simple-vars": "3.0.0",
"progress": "1.1.8",
"progress-bar-webpack-plugin": "1.9.1",
"raw-loader": "0.5.1",
"react-addons-perf": "15.4.1",
"react-addons-test-utils": "15.4.1",
"react-hot-loader": "3.0.0-beta.6",
"react-intl-aggregate-webpack-plugin": "0.0.1",
"rucksack-css": "0.9.1",
"script-ext-html-webpack-plugin": "1.3.4",
"script-ext-html-webpack-plugin": "1.3.5",
"serviceworker-webpack-plugin": "0.1.7",
"sinon": "1.17.6",
"sinon-as-promised": "4.0.2",
"sinon-chai": "2.8.0",
"style-loader": "0.13.1",
"stylelint": "7.6.0",
"stylelint-config-standard": "15.0.0",
"stylelint": "7.7.0",
"stylelint-config-standard": "15.0.1",
"url-loader": "0.5.7",
"webpack": "2.2.0-rc.2",
"webpack-dev-middleware": "1.8.4",
"webpack-dev-middleware": "1.9.0",
"webpack-error-notification": "0.1.6",
"webpack-hot-middleware": "2.13.2",
"websocket": "1.0.23"
"webpack-hot-middleware": "2.14.0",
"websocket": "1.0.24"
},
"dependencies": {
"bignumber.js": "3.0.1",

View File

@ -25,7 +25,7 @@ export default class Encoder {
throw new Error('tokens should be array of Token');
}
const mediates = tokens.map((token) => Encoder.encodeToken(token));
const mediates = tokens.map((token, index) => Encoder.encodeToken(token, index));
const inits = mediates
.map((mediate, idx) => mediate.init(Mediate.offsetFor(mediates, idx)))
.join('');
@ -36,37 +36,40 @@ export default class Encoder {
return `${inits}${closings}`;
}
static encodeToken (token) {
static encodeToken (token, index = 0) {
if (!isInstanceOf(token, Token)) {
throw new Error('token should be instanceof Token');
}
switch (token.type) {
case 'address':
return new Mediate('raw', padAddress(token.value));
try {
switch (token.type) {
case 'address':
return new Mediate('raw', padAddress(token.value));
case 'int':
case 'uint':
return new Mediate('raw', padU32(token.value));
case 'int':
case 'uint':
return new Mediate('raw', padU32(token.value));
case 'bool':
return new Mediate('raw', padBool(token.value));
case 'bool':
return new Mediate('raw', padBool(token.value));
case 'fixedBytes':
return new Mediate('raw', padFixedBytes(token.value));
case 'fixedBytes':
return new Mediate('raw', padFixedBytes(token.value));
case 'bytes':
return new Mediate('prefixed', padBytes(token.value));
case 'bytes':
return new Mediate('prefixed', padBytes(token.value));
case 'string':
return new Mediate('prefixed', padString(token.value));
case 'string':
return new Mediate('prefixed', padString(token.value));
case 'fixedArray':
case 'array':
return new Mediate(token.type, token.value.map((token) => Encoder.encodeToken(token)));
default:
throw new Error(`Invalid token type ${token.type} in encodeToken`);
case 'fixedArray':
case 'array':
return new Mediate(token.type, token.value.map((token) => Encoder.encodeToken(token)));
}
} catch (e) {
throw new Error(`Cannot encode token #${index} [${token.type}: ${token.value}]. ${e.message}`);
}
throw new Error(`Invalid token type ${token.type} in encodeToken`);
}
}

View File

@ -41,6 +41,10 @@ export default class Interface {
}
encodeTokens (paramTypes, values) {
return Interface.encodeTokens(paramTypes, values);
}
static encodeTokens (paramTypes, values) {
const createToken = function (paramType, value) {
if (paramType.subtype) {
return new Token(paramType.type, value.map((entry) => createToken(paramType.subtype, entry)));

View File

@ -114,7 +114,11 @@ export default class Api {
}
})
.catch((error) => {
console.error('pollMethod', error);
// Don't print if the request is rejected: that's ok
if (error.type !== 'REQUEST_REJECTED') {
console.error('pollMethod', error);
}
reject(error);
});
};

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import Abi from '../../abi';
import Abi from '~/abi';
let nextSubscriptionId = 0;
@ -53,6 +53,10 @@ export default class Contract {
this._subscribedToBlock = false;
this._blockSubscriptionId = null;
if (api && api.patch && api.patch.contract) {
api.patch.contract(this);
}
}
get address () {
@ -90,8 +94,10 @@ export default class Contract {
}
deployEstimateGas (options, values) {
const _options = this._encodeOptions(this.constructors[0], options, values);
return this._api.eth
.estimateGas(this._encodeOptions(this.constructors[0], options, values))
.estimateGas(_options)
.then((gasEst) => {
return [gasEst, gasEst.mul(1.2)];
});
@ -115,8 +121,10 @@ export default class Contract {
setState({ state: 'postTransaction', gas });
const _options = this._encodeOptions(this.constructors[0], options, values);
return this._api.parity
.postTransaction(this._encodeOptions(this.constructors[0], options, values))
.postTransaction(_options)
.then((requestId) => {
setState({ state: 'checkRequest', requestId });
return this._pollCheckRequest(requestId);
@ -199,7 +207,7 @@ export default class Contract {
getCallData = (func, options, values) => {
let data = options.data;
const tokens = func ? this._abi.encodeTokens(func.inputParamTypes(), values) : null;
const tokens = func ? Abi.encodeTokens(func.inputParamTypes(), values) : null;
const call = tokens ? func.encodeCall(tokens) : null;
if (data && data.substr(0, 2) === '0x') {
@ -221,6 +229,8 @@ export default class Contract {
}
_bindFunction = (func) => {
func.contract = this;
func.call = (options, values = []) => {
const callParams = this._encodeOptions(func, this._addOptionsTo(options), values);
@ -233,13 +243,13 @@ export default class Contract {
if (!func.constant) {
func.postTransaction = (options, values = []) => {
return this._api.parity
.postTransaction(this._encodeOptions(func, this._addOptionsTo(options), values));
const _options = this._encodeOptions(func, this._addOptionsTo(options), values);
return this._api.parity.postTransaction(_options);
};
func.estimateGas = (options, values = []) => {
return this._api.eth
.estimateGas(this._encodeOptions(func, this._addOptionsTo(options), values));
const _options = this._encodeOptions(func, this._addOptionsTo(options), values);
return this._api.eth.estimateGas(_options);
};
}

View File

@ -209,7 +209,10 @@ export default class Ws extends JsonRpcBase {
if (result.error) {
this.error(event.data);
console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`);
// Don't print error if request rejected...
if (!/rejected/.test(result.error.message)) {
console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`);
}
const error = new TransportError(method, result.error.code, result.error.message);
reject(error);

View File

@ -47,8 +47,6 @@ export function decodeMethodInput (methodAbi, paramdata) {
throw new Error('Input to decodeMethodInput should be a hex value');
} else if (paramdata.substr(0, 2) === '0x') {
return decodeMethodInput(methodAbi, paramdata.slice(2));
} else if (paramdata.length % 64 !== 0) {
throw new Error('Parameter length in decodeMethodInput not a multiple of 64 characters');
}
}

View File

@ -48,10 +48,6 @@ describe('api/util/decode', () => {
expect(() => decodeMethodInput({}, 'invalid')).to.throw(/should be a hex value/);
});
it('throws on invalid lengths', () => {
expect(() => decodeMethodInput({}, DATA.slice(-32))).to.throw(/not a multiple of/);
});
it('correctly decodes valid inputs', () => {
expect(decodeMethodInput({
type: 'function',

View File

@ -36,6 +36,7 @@ import ContextProvider from '~/ui/ContextProvider';
import muiTheme from '~/ui/Theme';
import MainApplication from './main';
import { patchApi } from '~/util/tx';
import { setApi } from '~/redux/providers/apiActions';
import './environment';
@ -60,6 +61,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
}
const api = new SecureApi(`ws://${parityUrl}`, token);
patchApi(api);
ContractInstances.create(api);
const store = initStore(api, hashHistory);

View File

@ -14,264 +14,255 @@
// 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 ContentAdd from 'material-ui/svg-icons/content/add';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions';
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation';
import { AddIcon, CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { eip20, wallet } from '~/contracts/abi';
import Store from './store';
const ABI_TYPES = [
{
label: 'Token', readOnly: true, value: JSON.stringify(eip20),
type: 'token',
description: (<span>A standard <a href='https://github.com/ethereum/EIPs/issues/20' target='_blank'>ERC 20</a> token</span>)
},
{
label: 'Multisig Wallet', readOnly: true,
type: 'multisig',
value: JSON.stringify(wallet),
description: (<span>Official Multisig contract: <a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>see contract code</a></span>)
},
{
label: 'Custom Contract', value: '',
type: 'custom',
description: 'Contract created from custom ABI'
}
];
const STEPS = [ 'choose a contract type', 'enter contract details' ];
export default class AddContract extends Component {
@observer
class AddContract extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
contracts: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func
};
state = {
abi: '',
abiError: ERRORS.invalidAbi,
abiType: ABI_TYPES[2],
abiTypeIndex: 2,
abiParsed: null,
address: '',
addressError: ERRORS.invalidAddress,
name: '',
nameError: ERRORS.invalidName,
description: '',
step: 0
};
componentDidMount () {
this.onChangeABIType(null, this.state.abiTypeIndex);
}
store = new Store(this.context.api, this.props.contracts);
render () {
const { step } = this.state;
const { step } = this.store;
return (
<Modal
visible
actions={ this.renderDialogActions() }
steps={ STEPS }
current={ step }
>
{ this.renderStep(step) }
steps={ [
<FormattedMessage
id='addContract.title.type'
defaultMessage='choose a contract type'
key='type' />,
<FormattedMessage
id='addContract.title.details'
defaultMessage='enter contract details'
key='details' />
] }
visible>
{ this.renderStep() }
</Modal>
);
}
renderStep (step) {
renderStep () {
const { step } = this.store;
switch (step) {
case 0:
return this.renderContractTypeSelector();
default:
return this.renderFields();
}
}
renderContractTypeSelector () {
const { abiTypeIndex } = this.state;
const { abiTypeIndex, abiTypes } = this.store;
return (
<RadioButtons
name='contractType'
value={ abiTypeIndex }
values={ this.getAbiTypes() }
values={ abiTypes }
onChange={ this.onChangeABIType }
/>
);
}
renderDialogActions () {
const { addressError, nameError, step } = this.state;
const hasError = !!(addressError || nameError);
const { step } = this.store;
const cancelBtn = (
<Button
icon={ <ContentClear /> }
label='Cancel'
icon={ <CancelIcon /> }
key='cancel'
label={
<FormattedMessage
id='addContract.button.cancel'
defaultMessage='Cancel' />
}
onClick={ this.onClose } />
);
if (step === 0) {
const nextBtn = (
return [
cancelBtn,
<Button
icon={ <NavigationArrowForward /> }
label='Next'
icon={ <NextIcon /> }
key='next'
label={
<FormattedMessage
id='addContract.button.next'
defaultMessage='Next' />
}
onClick={ this.onNext } />
);
return [ cancelBtn, nextBtn ];
];
}
const prevBtn = (
return [
cancelBtn,
<Button
icon={ <NavigationArrowBack /> }
label='Back'
onClick={ this.onPrev } />
);
const addBtn = (
icon={ <PrevIcon /> }
key='prev'
label={
<FormattedMessage
id='addContract.button.prev'
defaultMessage='Back' />
}
onClick={ this.onPrev } />,
<Button
icon={ <ContentAdd /> }
label='Add Contract'
disabled={ hasError }
icon={ <AddIcon /> }
key='add'
label={
<FormattedMessage
id='addContract.button.add'
defaultMessage='Add Contract' />
}
disabled={ this.store.hasError }
onClick={ this.onAdd } />
);
return [ cancelBtn, prevBtn, addBtn ];
];
}
renderFields () {
const { abi, abiError, address, addressError, description, name, nameError, abiType } = this.state;
const { abi, abiError, abiType, address, addressError, description, name, nameError } = this.store;
return (
<Form>
<InputAddress
label='network address'
hint='the network address for the contract'
error={ addressError }
value={ address }
onSubmit={ this.onEditAddress }
hint={
<FormattedMessage
id='addContract.address.hint'
defaultMessage='the network address for the contract' />
}
label={
<FormattedMessage
id='addContract.address.label'
defaultMessage='network address' />
}
onChange={ this.onChangeAddress }
/>
onSubmit={ this.onEditAddress }
value={ address } />
<Input
label='contract name'
hint='a descriptive name for the contract'
error={ nameError }
value={ name }
onSubmit={ this.onEditName } />
hint={
<FormattedMessage
id='addContract.name.hint'
defaultMessage='a descriptive name for the contract' />
}
label={
<FormattedMessage
id='addContract.name.label'
defaultMessage='contract name' />
}
onSubmit={ this.onEditName }
value={ name } />
<Input
multiLine
rows={ 1 }
label='(optional) contract description'
hint='an expanded description for the entry'
value={ description }
onSubmit={ this.onEditDescription } />
hint={
<FormattedMessage
id='addContract.description.hint'
defaultMessage='an expanded description for the entry' />
}
label={
<FormattedMessage
id='addContract.description.label'
defaultMessage='(optional) contract description' />
}
onSubmit={ this.onEditDescription }
value={ description } />
<Input
label='contract abi'
hint='the abi for the contract'
error={ abiError }
value={ abi }
readOnly={ abiType.readOnly }
hint={
<FormattedMessage
id='addContract.abi.hint'
defaultMessage='the abi for the contract' />
}
label={
<FormattedMessage
id='addContract.abi.label'
defaultMessage='contract abi' />
}
onSubmit={ this.onEditAbi }
/>
readOnly={ abiType.readOnly }
value={ abi } />
</Form>
);
}
getAbiTypes () {
return ABI_TYPES.map((type, index) => ({
label: type.label,
description: type.description,
key: index,
...type
}));
}
onNext = () => {
this.setState({ step: this.state.step + 1 });
this.store.nextStep();
}
onPrev = () => {
this.setState({ step: this.state.step - 1 });
this.store.prevStep();
}
onChangeABIType = (value, index) => {
const abiType = value || ABI_TYPES[index];
this.setState({ abiTypeIndex: index, abiType });
this.onEditAbi(abiType.value);
this.store.setAbiTypeIndex(index);
}
onEditAbi = (abiIn) => {
const { api } = this.context;
const { abi, abiError, abiParsed } = validateAbi(abiIn, api);
this.setState({ abi, abiError, abiParsed });
onEditAbi = (abi) => {
this.store.setAbi(abi);
}
onChangeAddress = (event, value) => {
this.onEditAddress(value);
onChangeAddress = (event, address) => {
this.onEditAddress(address);
}
onEditAddress = (_address) => {
const { contracts } = this.props;
let { address, addressError } = validateAddress(_address);
if (!addressError) {
const contract = contracts[address];
if (contract) {
addressError = ERRORS.duplicateAddress;
}
}
this.setState({
address,
addressError
});
onEditAddress = (address) => {
this.store.setAddress(address);
}
onEditDescription = (description) => {
this.setState({ description });
this.store.setDescription(description);
}
onEditName = (name) => {
this.setState(validateName(name));
this.store.setName(name);
}
onAdd = () => {
const { api } = this.context;
const { abiParsed, address, name, description, abiType } = this.state;
Promise.all([
api.parity.setAccountName(address, name),
api.parity.setAccountMeta(address, {
contract: true,
deleted: false,
timestamp: Date.now(),
abi: abiParsed,
type: abiType.type,
description
return this.store
.addContract()
.then(() => {
this.onClose();
})
]).catch((error) => {
console.error('onAdd', error);
});
this.props.onClose();
.catch((error) => {
this.props.newError(error);
});
}
onClose = () => {
this.props.onClose();
}
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
newError
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(AddContract);

View File

@ -0,0 +1,84 @@
// 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 AddContract from './';
import { CONTRACTS, createApi, createRedux } from './addContract.test.js';
let api;
let component;
let instance;
let onClose;
let reduxStore;
function render (props = {}) {
api = createApi();
onClose = sinon.stub();
reduxStore = createRedux();
component = shallow(
<AddContract
{ ...props }
contracts={ CONTRACTS }
onClose={ onClose } />,
{ context: { store: reduxStore } }
).find('AddContract').shallow({ context: { api } });
instance = component.instance();
return component;
}
describe('modals/AddContract', () => {
describe('rendering', () => {
beforeEach(() => {
render();
});
it('renders the defauls', () => {
expect(component).to.be.ok;
});
});
describe('onAdd', () => {
it('calls store addContract', () => {
sinon.stub(instance.store, 'addContract').resolves(true);
return instance.onAdd().then(() => {
expect(instance.store.addContract).to.have.been.called;
instance.store.addContract.restore();
});
});
it('calls closes dialog on success', () => {
sinon.stub(instance.store, 'addContract').resolves(true);
return instance.onAdd().then(() => {
expect(onClose).to.have.been.called;
instance.store.addContract.restore();
});
});
it('adds newError on failure', () => {
sinon.stub(instance.store, 'addContract').rejects('test');
return instance.onAdd().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
instance.store.addContract.restore();
});
});
});
});

View File

@ -0,0 +1,49 @@
// 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';
const ABI = '[{"constant":true,"inputs":[],"name":"totalDonated","outputs":[{"name":"","type":"uint256"}],"type":"function"}]';
const CONTRACTS = {
'0x1234567890123456789012345678901234567890': {}
};
function createApi () {
return {
parity: {
setAccountMeta: sinon.stub().resolves(),
setAccountName: sinon.stub().resolves()
}
};
}
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {};
}
};
}
export {
ABI,
CONTRACTS,
createApi,
createRedux
};

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 { action, computed, observable, transaction } from 'mobx';
import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation';
import { ABI_TYPES } from './types';
export default class Store {
@observable abi = '';
@observable abiError = ERRORS.invalidAbi;
@observable abiParsed = null;
@observable abiTypes = ABI_TYPES;
@observable abiTypeIndex = 0;
@observable address = '';
@observable addressError = ERRORS.invalidAddress;
@observable description = '';
@observable name = '';
@observable nameError = ERRORS.invalidName;
@observable step = 0;
constructor (api, contracts) {
this._api = api;
this._contracts = contracts;
this.setAbiTypeIndex(2);
}
@computed get abiType () {
return this.abiTypes[this.abiTypeIndex];
}
@computed get hasError () {
return !!(this.abiError || this.addressError || this.nameError);
}
@action nextStep = () => {
this.step++;
}
@action prevStep = () => {
this.step--;
}
@action setAbi = (_abi) => {
const { abi, abiError, abiParsed } = validateAbi(_abi);
transaction(() => {
this.abi = abi;
this.abiError = abiError;
this.abiParsed = abiParsed;
});
}
@action setAbiTypeIndex = (abiTypeIndex) => {
transaction(() => {
this.abiTypeIndex = abiTypeIndex;
this.setAbi(this.abiTypes[abiTypeIndex].value);
});
}
@action setAddress = (_address) => {
let { address, addressError } = validateAddress(_address);
if (!addressError) {
const contract = this._contracts[address];
if (contract) {
addressError = ERRORS.duplicateAddress;
}
}
transaction(() => {
this.address = address;
this.addressError = addressError;
});
}
@action setDescription = (description) => {
this.description = description;
}
@action setName = (_name) => {
const { name, nameError } = validateName(_name);
transaction(() => {
this.name = name;
this.nameError = nameError;
});
}
addContract () {
const meta = {
contract: true,
deleted: false,
timestamp: Date.now(),
abi: this.abiParsed,
type: this.abiType.type,
description: this.description
};
return Promise
.all([
this._api.parity.setAccountName(this.address, this.name),
this._api.parity.setAccountMeta(this.address, meta)
])
.catch((error) => {
console.error('addContract', error);
throw error;
});
}
}

View File

@ -0,0 +1,171 @@
// 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 Store from './store';
import { ABI, CONTRACTS, createApi } from './addContract.test.js';
const INVALID_ADDR = '0x123';
const VALID_ADDR = '0x5A5eFF38DA95b0D58b6C616f2699168B480953C9';
const DUPE_ADDR = Object.keys(CONTRACTS)[0];
let api;
let store;
function createStore () {
api = createApi();
store = new Store(api, CONTRACTS);
}
describe('modals/AddContract/Store', () => {
beforeEach(() => {
createStore();
});
describe('constructor', () => {
it('creates an instance', () => {
expect(store).to.be.ok;
});
it('defaults to custom ABI', () => {
expect(store.abiType.type).to.equal('custom');
});
});
describe('@actions', () => {
describe('nextStep/prevStep', () => {
it('moves to the next/prev step', () => {
expect(store.step).to.equal(0);
store.nextStep();
expect(store.step).to.equal(1);
store.prevStep();
expect(store.step).to.equal(0);
});
});
describe('setAbiTypeIndex', () => {
beforeEach(() => {
store.setAbiTypeIndex(1);
});
it('changes the index', () => {
expect(store.abiTypeIndex).to.equal(1);
});
it('changes the abi', () => {
expect(store.abi).to.deep.equal(store.abiTypes[1].value);
});
});
describe('setAddress', () => {
it('sets a valid address', () => {
store.setAddress(VALID_ADDR);
expect(store.address).to.equal(VALID_ADDR);
expect(store.addressError).to.be.null;
});
it('sets the error on invalid address', () => {
store.setAddress(INVALID_ADDR);
expect(store.address).to.equal(INVALID_ADDR);
expect(store.addressError).not.to.be.null;
});
it('sets the error on suplicate address', () => {
store.setAddress(DUPE_ADDR);
expect(store.address).to.equal(DUPE_ADDR);
expect(store.addressError).not.to.be.null;
});
});
describe('setDescription', () => {
it('sets the description', () => {
store.setDescription('test description');
expect(store.description).to.equal('test description');
});
});
describe('setName', () => {
it('sets the name', () => {
store.setName('some name');
expect(store.name).to.equal('some name');
expect(store.nameError).to.be.null;
});
it('sets the error', () => {
store.setName('s');
expect(store.name).to.equal('s');
expect(store.nameError).not.to.be.null;
});
});
});
describe('@computed', () => {
describe('abiType', () => {
it('matches the index', () => {
expect(store.abiType).to.deep.equal(store.abiTypes[2]);
});
});
describe('hasError', () => {
beforeEach(() => {
store.setAddress(VALID_ADDR);
store.setName('valid name');
store.setAbi(ABI);
});
it('is false with no errors', () => {
expect(store.hasError).to.be.false;
});
it('is true with address error', () => {
store.setAddress(DUPE_ADDR);
expect(store.hasError).to.be.true;
});
it('is true with name error', () => {
store.setName('s');
expect(store.hasError).to.be.true;
});
it('is true with abi error', () => {
store.setAbi('');
expect(store.hasError).to.be.true;
});
});
});
describe('interactions', () => {
describe('addContract', () => {
beforeEach(() => {
store.setAddress(VALID_ADDR);
store.setName('valid name');
store.setAbi(ABI);
});
it('sets the account name', () => {
return store.addContract().then(() => {
expect(api.parity.setAccountName).to.have.been.calledWith(VALID_ADDR, 'valid name');
});
});
it('sets the account meta', () => {
return store.addContract().then(() => {
expect(api.parity.setAccountMeta).to.have.been.called;
});
});
});
});
});

View File

@ -0,0 +1,89 @@
// 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 from 'react';
import { FormattedMessage } from 'react-intl';
import { eip20, wallet } from '~/contracts/abi';
const ABI_TYPES = [
{
description: (
<FormattedMessage
id='addContract.abiType.token.description'
defaultMessage='A standard {erc20} token'
values={ {
erc20: (
<a href='https://github.com/ethereum/EIPs/issues/20' target='_blank'>
<FormattedMessage
id='addContract.abiType.token.erc20'
defaultMessage='ERC 20' />
</a>
)
} } />
),
label: (
<FormattedMessage
id='addContract.abiType.token.label'
defaultMessage='Token' />
),
readOnly: true,
type: 'token',
value: JSON.stringify(eip20)
},
{
description: (
<FormattedMessage
id='addContract.abiType.multisigWallet.description'
defaultMessage='Ethereum Multisig contract {link}'
values={ {
link: (
<a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>
<FormattedMessage
id='addContract.abiType.multisigWallet.link'
defaultMessage='see contract code' />
</a>
)
} } />
),
label: (
<FormattedMessage
id='addContract.abiType.multisigWallet.label'
defaultMessage='Multisig Wallet' />
),
readOnly: true,
type: 'multisig',
value: JSON.stringify(wallet)
},
{
description: (
<FormattedMessage
id='addContract.abiType.custom.description'
defaultMessage='Contract created from custom ABI' />
),
label: (
<FormattedMessage
id='addContract.abiType.custom.label'
defaultMessage='Custom Contract' />
),
type: 'custom',
value: ''
}
];
export {
ABI_TYPES
};

View File

@ -18,28 +18,33 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
export default {
noFile:
noFile: (
<FormattedMessage
id='createAccount.error.noFile'
defaultMessage='select a valid wallet file to import' />,
defaultMessage='select a valid wallet file to import' />
),
noKey:
noKey: (
<FormattedMessage
id='createAccount.error.noKey'
defaultMessage='you need to provide the raw private key' />,
defaultMessage='you need to provide the raw private key' />
),
noMatchPassword:
noMatchPassword: (
<FormattedMessage
id='createAccount.error.noMatchPassword'
defaultMessage='the supplied passwords does not match' />,
defaultMessage='the supplied passwords does not match' />
),
noName:
noName: (
<FormattedMessage
id='createAccount.error.noName'
defaultMessage='you need to specify a valid name for the account' />,
defaultMessage='you need to specify a valid name for the account' />
),
invalidKey:
invalidKey: (
<FormattedMessage
id='createAccount.error.invalidKey'
defaultMessage='the raw key needs to be hex, 64 characters in length and contain the prefix "0x"' />
)
};

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { omitBy } from 'lodash';
import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui';
@ -73,6 +74,9 @@ export default class WalletDetails extends Component {
renderMultisigDetails () {
const { accounts, wallet, errors } = this.props;
// Wallets cannot create contracts
const _accounts = omitBy(accounts, (a) => a.wallet);
return (
<Form>
<AddressSelect
@ -81,7 +85,7 @@ export default class WalletDetails extends Component {
value={ wallet.account }
error={ errors.account }
onChange={ this.onAccoutChange }
accounts={ accounts }
accounts={ _accounts }
/>
<Input

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { pick } from 'lodash';
import { pick, omitBy } from 'lodash';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
@ -34,29 +34,33 @@ import { ERROR_CODES } from '~/api/transport/error';
const STEPS = {
CONTRACT_DETAILS: {
title:
title: (
<FormattedMessage
id='deployContract.title.details'
defaultMessage='contract details' />
)
},
CONTRACT_PARAMETERS: {
title:
title: (
<FormattedMessage
id='deployContract.title.parameters'
defaultMessage='contract parameters' />
)
},
DEPLOYMENT: {
waiting: true,
title:
title: (
<FormattedMessage
id='deployContract.title.deployment'
defaultMessage='deployment' />
)
},
COMPLETED: {
title:
title: (
<FormattedMessage
id='deployContract.title.completed'
defaultMessage='completed' />
)
}
};
@ -495,48 +499,53 @@ class DeployContract extends Component {
case 'estimateGas':
case 'postTransaction':
this.setState({
deployState:
deployState: (
<FormattedMessage
id='deployContract.state.preparing'
defaultMessage='Preparing transaction for network transmission' />
)
});
return;
case 'checkRequest':
this.setState({
deployState:
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:
deployState: (
<FormattedMessage
id='deployContract.state.waitReceipt'
defaultMessage='Waiting for the contract deployment transaction receipt' />
)
});
return;
case 'hasReceipt':
case 'getCode':
this.setState({
deployState:
deployState: (
<FormattedMessage
id='deployContract.state.validatingCode'
defaultMessage='Validating the deployed contract code' />
)
});
return;
case 'completed':
this.setState({
deployState:
deployState: (
<FormattedMessage
id='deployContract.state.completed'
defaultMessage='The contract deployment has been completed' />
)
});
return;
@ -552,13 +561,19 @@ class DeployContract extends Component {
}
function mapStateToProps (initState, initProps) {
const fromAddresses = Object.keys(initProps.accounts);
const { accounts } = initProps;
// Skip Wallet accounts : they can't create Contracts
const _accounts = omitBy(accounts, (a) => a.wallet);
const fromAddresses = Object.keys(_accounts);
return (state) => {
const balances = pick(state.balances.balances, fromAddresses);
const { gasLimit } = state.nodeStatus;
return {
accounts: _accounts,
balances,
gasLimit
};

View File

@ -17,20 +17,24 @@
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 { newError } from '~/redux/actions';
import { Button, Form, Input, InputChip, Modal } from '~/ui';
import { CancelIcon, SaveIcon } from '~/ui/Icons';
import Store from './store';
@observer
export default class EditMeta extends Component {
class EditMeta extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
account: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
}
@ -85,7 +89,7 @@ export default class EditMeta extends Component {
defaultMessage='(optional) tags' />
}
onTokensChange={ this.store.setTags }
tokens={ tags } />
tokens={ tags.slice() } />
</Form>
</Modal>
);
@ -138,6 +142,20 @@ export default class EditMeta extends Component {
return this.store
.save()
.then(() => this.props.onClose());
.then(() => this.props.onClose())
.catch((error) => {
this.props.newError(error);
});
}
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
newError
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(EditMeta);

View File

@ -20,30 +20,27 @@ import sinon from 'sinon';
import EditMeta from './';
import { ACCOUNT } from './editMeta.test.js';
import { ACCOUNT, createApi, createRedux } from './editMeta.test.js';
let api;
let component;
let instance;
let onClose;
let reduxStore;
function render (props) {
api = createApi();
onClose = sinon.stub();
reduxStore = createRedux();
component = shallow(
<EditMeta
{ ...props }
account={ ACCOUNT }
onClose={ onClose } />,
{
context: {
api: {
parity: {
setAccountName: sinon.stub().resolves(),
setAccountMeta: sinon.stub().resolves()
}
}
}
}
);
{ context: { store: reduxStore } }
).find('EditMeta').shallow({ context: { api } });
instance = component.instance();
return component;
}
@ -61,15 +58,29 @@ describe('modals/EditMeta', () => {
});
describe('onSave', () => {
it('calls store.save() & props.onClose', () => {
const instance = component.instance();
it('calls store.save', () => {
sinon.spy(instance.store, 'save');
instance.onSave().then(() => {
return instance.onSave().then(() => {
expect(instance.store.save).to.have.been.called;
instance.store.save.restore();
});
});
it('closes the dialog on success', () => {
return instance.onSave().then(() => {
expect(onClose).to.have.been.called;
});
});
it('adds newError on failure', () => {
sinon.stub(instance.store, 'save').rejects('test');
return instance.onSave().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
instance.store.save.restore();
});
});
});
});
});

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 sinon from 'sinon';
const ACCOUNT = {
address: '0x123456789a123456789a123456789a123456789a',
meta: {
@ -39,7 +41,28 @@ const ADDRESS = {
name: 'Random address'
};
function createApi () {
return {
parity: {
setAccountName: sinon.stub().resolves(),
setAccountMeta: sinon.stub().resolves()
}
};
}
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {};
}
};
}
export {
ACCOUNT,
ADDRESS
ADDRESS,
createApi,
createRedux
};

View File

@ -14,34 +14,35 @@
// 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, computed, observable, toJS, transaction } from 'mobx';
import { action, computed, observable, transaction } from 'mobx';
import { newError } from '~/redux/actions';
import { validateName } from '~/util/validation';
export default class Store {
@observable address = null;
@observable isAccount = false;
@observable description = null;
@observable meta = {};
@observable meta = null;
@observable name = null;
@observable nameError = null;
@observable passwordHint = null;
@observable tags = [];
@observable tags = null;
constructor (api, account) {
const { address, name, meta, uuid } = account;
this._api = api;
this.isAccount = !!uuid;
this.address = address;
this.meta = Object.assign({}, meta || {});
this.name = name || '';
transaction(() => {
this.isAccount = !!uuid;
this.address = address;
this.meta = meta || {};
this.name = name || '';
this.description = this.meta.description || '';
this.passwordHint = this.meta.passwordHint || '';
this.tags = [].concat((meta || {}).tags || []);
this.description = this.meta.description || '';
this.passwordHint = this.meta.passwordHint || '';
this.tags = this.meta.tags && this.meta.tags.peek() || [];
});
}
@computed get hasError () {
@ -70,7 +71,7 @@ export default class Store {
}
@action setTags = (tags) => {
this.tags = [].concat(tags);
this.tags = tags.slice();
}
save () {
@ -86,12 +87,11 @@ export default class Store {
return Promise
.all([
this._api.parity.setAccountName(this.address, this.name),
this._api.parity.setAccountMeta(this.address, Object.assign({}, toJS(this.meta), meta))
this._api.parity.setAccountMeta(this.address, Object.assign({}, this.meta, meta))
])
.catch((error) => {
console.error('onSave', error);
newError(error);
throw error;
});
}
}

View File

@ -14,22 +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 { toJS } from 'mobx';
import sinon from 'sinon';
import Store from './store';
import { ACCOUNT, ADDRESS } from './editMeta.test.js';
import { ACCOUNT, ADDRESS, createApi } from './editMeta.test.js';
let api;
let store;
function createStore (account) {
api = {
parity: {
setAccountName: sinon.stub().resolves(),
setAccountMeta: sinon.stub().resolves()
}
};
api = createApi();
store = new Store(api, account);
@ -56,12 +48,12 @@ describe('modals/EditMeta/Store', () => {
});
it('extracts the tags', () => {
expect(store.tags.peek()).to.deep.equal(ACCOUNT.meta.tags);
expect(store.tags).to.deep.equal(ACCOUNT.meta.tags);
});
describe('meta', () => {
it('extracts the full meta', () => {
expect(toJS(store.meta)).to.deep.equal(ACCOUNT.meta);
expect(store.meta).to.deep.equal(ACCOUNT.meta);
});
it('extracts the description', () => {

View File

@ -22,7 +22,7 @@ import { ContextProvider, muiTheme } from '~/ui';
import DetailsStep from './';
import { STORE, CONTRACT } from '../executeContract.test.js';
import { createApi, STORE, CONTRACT } from '../executeContract.test.js';
let component;
let onAmountChange;
@ -41,7 +41,7 @@ function render (props) {
onValueChange = sinon.stub();
component = mount(
<ContextProvider api={ {} } muiTheme={ muiTheme } store={ STORE }>
<ContextProvider api={ createApi() } muiTheme={ muiTheme } store={ STORE }>
<DetailsStep
{ ...props }
contract={ CONTRACT }

View File

@ -39,26 +39,31 @@ const STEP_BUSY_OR_ADVANCED = 1;
const STEP_BUSY = 2;
const TITLES = {
transfer:
transfer: (
<FormattedMessage
id='executeContract.steps.transfer'
defaultMessage='function details' />,
sending:
defaultMessage='function details' />
),
sending: (
<FormattedMessage
id='executeContract.steps.sending'
defaultMessage='sending' />,
complete:
defaultMessage='sending' />
),
complete: (
<FormattedMessage
id='executeContract.steps.complete'
defaultMessage='complete' />,
advanced:
defaultMessage='complete' />
),
advanced: (
<FormattedMessage
id='executeContract.steps.advanced'
defaultMessage='advanced options' />,
rejected:
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];
@ -384,6 +389,7 @@ class ExecuteContract extends Component {
const { advancedOptions, amount, func, minBlock, values } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
const finalstep = steps.length - 1;
const options = {
gas: this.gasStore.gas,
gasPrice: this.gasStore.price,
@ -398,10 +404,11 @@ class ExecuteContract extends Component {
.postTransaction(options, values)
.then((requestId) => {
this.setState({
busyState:
busyState: (
<FormattedMessage
id='executeContract.busy.waitAuth'
defaultMessage='Waiting for authorization in the Parity Signer' />
)
});
return api
@ -420,10 +427,11 @@ class ExecuteContract extends Component {
sending: false,
step: finalstep,
txhash,
busyState:
busyState: (
<FormattedMessage
id='executeContract.busy.posted'
defaultMessage='Your transaction has been posted to the network' />
)
});
})
.catch((error) => {

View File

@ -20,7 +20,7 @@ import sinon from 'sinon';
import ExecuteContract from './';
import { CONTRACT, STORE } from './executeContract.test.js';
import { createApi, CONTRACT, STORE } from './executeContract.test.js';
let component;
let onClose;
@ -36,7 +36,7 @@ function render (props) {
contract={ CONTRACT }
onClose={ onClose }
onFromAddressChange={ onFromAddressChange } />,
{ context: { api: {}, store: STORE } }
{ context: { api: createApi(), store: STORE } }
).find('ExecuteContract').shallow();
return component;

View File

@ -64,7 +64,19 @@ const STORE = {
}
};
function createApi (result = true) {
return {
parity: {
registryAddress: sinon.stub().resolves('0x0000000000000000000000000000000000000000')
},
util: {
sha3: sinon.stub().resolves('0x0000000000000000000000000000000000000000')
}
};
}
export {
createApi,
CONTRACT,
STORE
};

View File

@ -14,26 +14,36 @@
// 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 ContentClear from 'material-ui/svg-icons/content/clear';
import CheckIcon from 'material-ui/svg-icons/navigation/check';
import SendIcon from 'material-ui/svg-icons/content/send';
import { Tabs, Tab } from 'material-ui/Tabs';
import Paper from 'material-ui/Paper';
import { Tabs, Tab } from 'material-ui/Tabs';
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 { showSnackbar } from '~/redux/providers/snackbarActions';
import Form, { Input } from '~/ui/Form';
import { newError, openSnackbar } from '~/redux/actions';
import { Button, Modal, IdentityName, IdentityIcon } from '~/ui';
import Form, { Input } from '~/ui/Form';
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
import Store, { CHANGE_ACTION, TEST_ACTION } from './store';
import styles from './passwordManager.css';
const TEST_ACTION = 'TEST_ACTION';
const CHANGE_ACTION = 'CHANGE_ACTION';
const MSG_SUCCESS_STYLE = {
backgroundColor: 'rgba(174, 213, 129, 0.75)'
};
const MSG_FAILURE_STYLE = {
backgroundColor: 'rgba(229, 115, 115, 0.75)'
};
const TABS_INKBAR_STYLE = {
backgroundColor: 'rgba(255, 255, 255, 0.55)'
};
const TABS_ITEM_STYLE = {
backgroundColor: 'rgba(255, 255, 255, 0.05)'
};
@observer
class PasswordManager extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
@ -41,27 +51,22 @@ class PasswordManager extends Component {
static propTypes = {
account: PropTypes.object.isRequired,
showSnackbar: PropTypes.func.isRequired,
openSnackbar: PropTypes.func.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func
}
state = {
action: TEST_ACTION,
waiting: false,
showMessage: false,
message: { value: '', success: true },
currentPass: '',
newPass: '',
repeatNewPass: '',
repeatValid: true,
passwordHint: this.props.account.meta && this.props.account.meta.passwordHint || ''
}
store = new Store(this.context.api, this.props.account);
render () {
return (
<Modal
actions={ this.renderDialogActions() }
title='Password Manager'
title={
<FormattedMessage
id='passwordChange.title'
defaultMessage='Password Manager' />
}
visible>
{ this.renderAccount() }
{ this.renderPage() }
@ -71,150 +76,168 @@ class PasswordManager extends Component {
}
renderMessage () {
const { message, showMessage } = this.state;
const { infoMessage } = this.store;
const style = message.success
? {
backgroundColor: 'rgba(174, 213, 129, 0.75)'
}
: {
backgroundColor: 'rgba(229, 115, 115, 0.75)'
};
const classes = [ styles.message ];
if (!showMessage) {
classes.push(styles.hideMessage);
if (!infoMessage) {
return null;
}
return (
<Paper
zDepth={ 1 }
style={ style }
className={ classes.join(' ') }>
{ message.value }
className={ `${styles.message}` }
style={
infoMessage.success
? MSG_SUCCESS_STYLE
: MSG_FAILURE_STYLE
}
zDepth={ 1 }>
{ infoMessage.value }
</Paper>
);
}
renderAccount () {
const { account } = this.props;
const { address, meta } = account;
const passwordHint = meta && meta.passwordHint
? (
<span className={ styles.passwordHint }>
<span className={ styles.hintLabel }>Hint </span>
{ meta.passwordHint }
</span>
)
: null;
const { address, passwordHint } = this.store;
return (
<div className={ styles.accountContainer }>
<IdentityIcon
address={ address }
/>
<IdentityIcon address={ address } />
<div className={ styles.accountInfos }>
<IdentityName
className={ styles.accountName }
address={ address }
unknown
/>
className={ styles.accountName }
unknown />
<span className={ styles.accountAddress }>
{ address }
</span>
{ passwordHint }
<span className={ styles.passwordHint }>
<span className={ styles.hintLabel }>Hint </span>
{ passwordHint || '-' }
</span>
</div>
</div>
);
}
renderPage () {
const { account } = this.props;
const { waiting, repeatValid } = this.state;
const disabled = !!waiting;
const repeatError = repeatValid
? null
: 'the two passwords differ';
const { meta } = account;
const passwordHint = meta && meta.passwordHint || '';
const { busy, isRepeatValid, passwordHint } = this.store;
return (
<Tabs
inkBarStyle={ {
backgroundColor: 'rgba(255, 255, 255, 0.55)'
} }
tabItemContainerStyle={ {
backgroundColor: 'rgba(255, 255, 255, 0.05)'
} }
>
inkBarStyle={ TABS_INKBAR_STYLE }
tabItemContainerStyle={ TABS_ITEM_STYLE }>
<Tab
onActive={ this.handleTestActive }
label='Test Password'
>
<Form
className={ styles.form }
>
label={
<FormattedMessage
id='passwordChange.tabTest.label'
defaultMessage='Test Password' />
}
onActive={ this.onActivateTestTab }>
<Form className={ styles.form }>
<div>
<Input
label='password'
hint='your current password for this account'
type='password'
disabled={ busy }
hint={
<FormattedMessage
id='passwordChange.testPassword.hint'
defaultMessage='your account password' />
}
label={
<FormattedMessage
id='passwordChange.testPassword.label'
defaultMessage='password' />
}
onChange={ this.onEditTestPassword }
onSubmit={ this.testPassword }
submitOnBlur={ false }
disabled={ disabled }
onSubmit={ this.handleTestPassword }
onChange={ this.onEditCurrent } />
type='password' />
</div>
</Form>
</Tab>
<Tab
onActive={ this.handleChangeActive }
label='Change Password'
>
<Form
className={ styles.form }
>
label={
<FormattedMessage
id='passwordChange.tabChange.label'
defaultMessage='Change Password' />
}
onActive={ this.onActivateChangeTab }>
<Form className={ styles.form }>
<div>
<Input
label='current password'
hint='your current password for this account'
type='password'
disabled={ busy }
hint={
<FormattedMessage
id='passwordChange.currentPassword.hint'
defaultMessage='your current password for this account' />
}
label={
<FormattedMessage
id='passwordChange.currentPassword.label'
defaultMessage='current password' />
}
onChange={ this.onEditCurrentPassword }
onSubmit={ this.changePassword }
submitOnBlur={ false }
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditCurrent } />
type='password' />
<Input
label='(optional) new password hint'
hint='hint for the new password'
disabled={ busy }
hint={
<FormattedMessage
id='passwordChange.passwordHint.hint'
defaultMessage='hint for the new password' />
}
label={
<FormattedMessage
id='passwordChange.passwordHint.label'
defaultMessage='(optional) new password hint' />
}
onChange={ this.onEditNewPasswordHint }
onSubmit={ this.changePassword }
submitOnBlur={ false }
value={ passwordHint }
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditHint } />
value={ passwordHint } />
<div className={ styles.passwords }>
<div className={ styles.password }>
<Input
label='new password'
hint='the new password for this account'
type='password'
disabled={ busy }
hint={
<FormattedMessage
id='passwordChange.newPassword.hint'
defaultMessage='the new password for this account' />
}
label={
<FormattedMessage
id='passwordChange.newPassword.label'
defaultMessage='new password' />
}
onChange={ this.onEditNewPassword }
onSubmit={ this.changePassword }
submitOnBlur={ false }
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditNew } />
type='password' />
</div>
<div className={ styles.password }>
<Input
label='repeat new password'
hint='repeat the new password for this account'
type='password'
disabled={ busy }
error={
isRepeatValid
? null
: <FormattedMessage
id='passwordChange.repeatPassword.error'
defaultMessage='the supplied passwords do not match' />
}
hint={
<FormattedMessage
id='passwordChange.repeatPassword.hint'
defaultMessage='repeat the new password for this account' />
}
label={
<FormattedMessage
id='passwordChange.repeatPassword.label'
defaultMessage='repeat new password' />
}
onChange={ this.onEditNewPasswordRepeat }
onSubmit={ this.changePassword }
submitOnBlur={ false }
error={ repeatError }
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditRepeatNew } />
type='password' />
</div>
</div>
</div>
@ -225,172 +248,126 @@ class PasswordManager extends Component {
}
renderDialogActions () {
const { actionTab, busy, isRepeatValid } = this.store;
const { onClose } = this.props;
const { action, waiting, repeatValid } = this.state;
const cancelBtn = (
<Button
icon={ <ContentClear /> }
label='Cancel'
icon={ <CancelIcon /> }
key='cancel'
label={
<FormattedMessage
id='passwordChange.button.cancel'
defaultMessage='Cancel' />
}
onClick={ onClose } />
);
if (waiting) {
const waitingBtn = (
if (busy) {
return [
cancelBtn,
<Button
disabled
label='Wait...' />
);
return [ cancelBtn, waitingBtn ];
key='wait'
label={
<FormattedMessage
id='passwordChange.button.wait'
defaultMessage='Wait...' />
} />
];
}
if (action === TEST_ACTION) {
const testBtn = (
if (actionTab === TEST_ACTION) {
return [
cancelBtn,
<Button
icon={ <CheckIcon /> }
label='Test'
onClick={ this.handleTestPassword } />
);
return [ cancelBtn, testBtn ];
key='test'
label={
<FormattedMessage
id='passwordChange.button.test'
defaultMessage='Test' />
}
onClick={ this.testPassword } />
];
}
const changeBtn = (
return [
cancelBtn,
<Button
disabled={ !repeatValid }
disabled={ !isRepeatValid }
icon={ <SendIcon /> }
label='Change'
onClick={ this.handleChangePassword } />
);
return [ cancelBtn, changeBtn ];
key='change'
label={
<FormattedMessage
id='passwordChange.button.change'
defaultMessage='Change' />
}
onClick={ this.changePassword } />
];
}
onEditCurrent = (event, value) => {
this.setState({
currentPass: value,
showMessage: false
});
onActivateChangeTab = () => {
this.store.setActionTab(CHANGE_ACTION);
}
onEditNew = (event, value) => {
const repeatValid = value === this.state.repeatNewPass;
this.setState({
newPass: value,
showMessage: false,
repeatValid
});
onActivateTestTab = () => {
this.store.setActionTab(TEST_ACTION);
}
onEditRepeatNew = (event, value) => {
const repeatValid = value === this.state.newPass;
this.setState({
repeatNewPass: value,
showMessage: false,
repeatValid
});
onEditCurrentPassword = (event, password) => {
this.store.setPassword(password);
}
onEditHint = (event, value) => {
this.setState({
passwordHint: value,
showMessage: false
});
onEditNewPassword = (event, password) => {
this.store.setNewPassword(password);
}
handleTestActive = () => {
this.setState({
action: TEST_ACTION,
showMessage: false
});
onEditNewPasswordHint = (event, passwordHint) => {
this.store.setNewPasswordHint(passwordHint);
}
handleChangeActive = () => {
this.setState({
action: CHANGE_ACTION,
showMessage: false
});
onEditNewPasswordRepeat = (event, password) => {
this.store.setNewPasswordRepeat(password);
}
handleTestPassword = () => {
const { account } = this.props;
const { currentPass } = this.state;
onEditTestPassword = (event, password) => {
this.store.setValidatePassword(password);
}
this.setState({ waiting: true, showMessage: false });
this.context
.api.parity
.testPassword(account.address, currentPass)
.then(correct => {
const message = correct
? { value: 'This password is correct', success: true }
: { value: 'This password is not correct', success: false };
this.setState({ waiting: false, message, showMessage: true });
changePassword = () => {
return this.store
.changePassword()
.then((result) => {
if (result) {
this.props.openSnackbar(
<div>
<FormattedMessage
id='passwordChange.success'
defaultMessage='Your password has been successfully changed' />
</div>
);
this.props.onClose();
}
})
.catch(e => {
console.error('passwordManager::handleTestPassword', e);
this.setState({ waiting: false });
.catch((error) => {
this.props.newError(error);
});
}
handleChangePassword = () => {
const { account, showSnackbar, onClose } = this.props;
const { currentPass, newPass, repeatNewPass, passwordHint } = this.state;
if (repeatNewPass !== newPass) {
return;
}
this.setState({ waiting: true, showMessage: false });
this.context
.api.parity
.testPassword(account.address, currentPass)
.then(correct => {
if (!correct) {
const message = {
value: 'This provided current password is not correct',
success: false
};
this.setState({ waiting: false, message, showMessage: true });
return false;
}
const meta = Object.assign({}, account.meta, {
passwordHint
});
return Promise.all([
this.context
.api.parity
.setAccountMeta(account.address, meta),
this.context
.api.parity
.changePassword(account.address, currentPass, newPass)
])
.then(() => {
showSnackbar(<div>Your password has been successfully changed.</div>);
this.setState({ waiting: false, showMessage: false });
onClose();
});
})
.catch(e => {
console.error('passwordManager::handleChangePassword', e);
this.setState({ waiting: false });
testPassword = () => {
return this.store
.testPassword()
.catch((error) => {
this.props.newError(error);
});
}
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
showSnackbar
openSnackbar,
newError
}, dispatch);
}

View File

@ -0,0 +1,110 @@
// 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 PasswordManager from './';
import { ACCOUNT, createApi, createRedux } from './passwordManager.test.js';
let component;
let instance;
let onClose;
let reduxStore;
function render (props) {
onClose = sinon.stub();
reduxStore = createRedux();
component = shallow(
<PasswordManager
{ ...props }
account={ ACCOUNT }
onClose={ onClose } />,
{ context: { store: reduxStore } }
).find('PasswordManager').shallow({ context: { api: createApi() } });
instance = component.instance();
return component;
}
describe('modals/PasswordManager', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});
describe('actions', () => {
beforeEach(() => {
render();
});
describe('changePassword', () => {
it('calls store.changePassword', () => {
sinon.spy(instance.store, 'changePassword');
return instance.changePassword().then(() => {
expect(instance.store.changePassword).to.have.been.called;
instance.store.changePassword.restore();
});
});
it('closes the dialog on success', () => {
return instance.changePassword().then(() => {
expect(onClose).to.have.been.called;
});
});
it('shows snackbar on success', () => {
return instance.changePassword().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWithMatch({ type: 'openSnackbar' });
});
});
it('adds newError on failure', () => {
sinon.stub(instance.store, 'changePassword').rejects('test');
return instance.changePassword().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
instance.store.changePassword.restore();
});
});
});
describe('testPassword', () => {
it('calls store.testPassword', () => {
sinon.spy(instance.store, 'testPassword');
return instance.testPassword().then(() => {
expect(instance.store.testPassword).to.have.been.called;
instance.store.testPassword.restore();
});
});
it('adds newError on failure', () => {
sinon.stub(instance.store, 'testPassword').rejects('test');
return instance.testPassword().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
instance.store.testPassword.restore();
});
});
});
});
});

View File

@ -0,0 +1,54 @@
// 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';
const ACCOUNT = {
address: '0x123456789a123456789a123456789a123456789a',
meta: {
description: 'Call me bob',
passwordHint: 'some hint',
tags: ['testing']
},
name: 'Bobby',
uuid: '123-456'
};
function createApi (result = true) {
return {
parity: {
changePassword: sinon.stub().resolves(result),
setAccountMeta: sinon.stub().resolves(result),
testPassword: sinon.stub().resolves(result)
}
};
}
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {};
}
};
}
export {
ACCOUNT,
createApi,
createRedux
};

View File

@ -0,0 +1,161 @@
// 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, computed, observable, transaction } from 'mobx';
const CHANGE_ACTION = 'CHANGE_ACTION';
const TEST_ACTION = 'TEST_ACTION';
export default class Store {
@observable actionTab = TEST_ACTION;
@observable address = null;
@observable busy = false;
@observable infoMessage = null;
@observable meta = null;
@observable newPassword = '';
@observable newPasswordHint = '';
@observable newPasswordRepeat = '';
@observable password = '';
@observable passwordHint = '';
@observable validatePassword = '';
constructor (api, account) {
this._api = api;
this.address = account.address;
this.meta = account.meta || {};
this.passwordHint = this.meta.passwordHint || '';
}
@computed get isRepeatValid () {
return this.newPasswordRepeat === this.newPassword;
}
@action setActionTab = (actionTab) => {
transaction(() => {
this.actionTab = actionTab;
this.setInfoMessage();
});
}
@action setBusy = (busy, message) => {
transaction(() => {
this.busy = busy;
this.setInfoMessage(message);
});
}
@action setInfoMessage = (message = null) => {
this.infoMessage = message;
}
@action setPassword = (password) => {
transaction(() => {
this.password = password;
this.setInfoMessage();
});
}
@action setNewPassword = (password) => {
transaction(() => {
this.newPassword = password;
this.setInfoMessage();
});
}
@action setNewPasswordHint = (passwordHint) => {
transaction(() => {
this.newPasswordHint = passwordHint;
this.setInfoMessage();
});
}
@action setNewPasswordRepeat = (password) => {
transaction(() => {
this.newPasswordRepeat = password;
this.setInfoMessage();
});
}
@action setValidatePassword = (password) => {
transaction(() => {
this.validatePassword = password;
this.setInfoMessage();
});
}
changePassword = () => {
if (!this.isRepeatValid) {
return Promise.resolve(false);
}
this.setBusy(true);
return this
.testPassword(this.password)
.then((result) => {
if (!result) {
return false;
}
const meta = Object.assign({}, this.meta, {
passwordHint: this.newPasswordHint
});
return Promise
.all([
this._api.parity.setAccountMeta(this.address, meta),
this._api.parity.changePassword(this.address, this.password, this.newPassword)
])
.then(() => {
this.setBusy(false);
return true;
});
})
.catch((error) => {
console.error('changePassword', error);
this.setBusy(false);
throw error;
});
}
testPassword = (password) => {
this.setBusy(false);
return this._api.parity
.testPassword(this.address, password || this.validatePassword)
.then((success) => {
this.setBusy(false, {
success,
value: success
? 'This password is correct'
: 'This password is not correct'
});
return success;
})
.catch((error) => {
console.error('testPassword', error);
this.setBusy(false);
throw error;
});
}
}
export {
CHANGE_ACTION,
TEST_ACTION
};

View File

@ -0,0 +1,103 @@
// 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 Store from './store';
import { ACCOUNT, createApi } from './passwordManager.test.js';
let api;
let store;
function createStore (account) {
api = createApi();
store = new Store(api, account);
return store;
}
describe('modals/PasswordManager/Store', () => {
beforeEach(() => {
createStore(ACCOUNT);
});
describe('constructor', () => {
it('extracts the address', () => {
expect(store.address).to.equal(ACCOUNT.address);
});
describe('meta', () => {
it('extracts the full meta', () => {
expect(store.meta).to.deep.equal(ACCOUNT.meta);
});
it('extracts the passwordHint', () => {
expect(store.passwordHint).to.equal(ACCOUNT.meta.passwordHint);
});
});
});
describe('operations', () => {
const CUR_PASSWORD = 'aPassW0rd';
const NEW_PASSWORD = 'br@ndNEW';
const NEW_HINT = 'something new to test';
describe('changePassword', () => {
beforeEach(() => {
store.setPassword(CUR_PASSWORD);
store.setNewPasswordHint(NEW_HINT);
store.setNewPassword(NEW_PASSWORD);
store.setNewPasswordRepeat(NEW_PASSWORD);
});
it('calls parity.testPassword with current password', () => {
return store.changePassword().then(() => {
expect(api.parity.testPassword).to.have.been.calledWith(ACCOUNT.address, CUR_PASSWORD);
});
});
it('calls parity.setAccountMeta with new hint', () => {
return store.changePassword().then(() => {
expect(api.parity.setAccountMeta).to.have.been.calledWith(ACCOUNT.address, Object.assign({}, ACCOUNT.meta, {
passwordHint: NEW_HINT
}));
});
});
it('calls parity.changePassword with the new password', () => {
return store.changePassword().then(() => {
expect(api.parity.changePassword).to.have.been.calledWith(ACCOUNT.address, CUR_PASSWORD, NEW_PASSWORD);
});
});
});
describe('testPassword', () => {
beforeEach(() => {
store.setValidatePassword(CUR_PASSWORD);
});
it('calls parity.testPassword', () => {
return store.testPassword().then(() => {
expect(api.parity.testPassword).to.have.been.calledWith(ACCOUNT.address, CUR_PASSWORD);
});
});
it('sets the infoMessage for success/failure', () => {
return store.testPassword().then(() => {
expect(store.infoMessage).not.to.be.null;
});
});
});
});
});

View File

@ -383,9 +383,7 @@ export default class TransferStore {
const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag);
const format = new BigNumber(senderBalance.token.format || 1);
const available = isWallet
? this.api.util.fromWei(new BigNumber(senderBalance.value))
: (new BigNumber(senderBalance.value)).div(format);
const available = new BigNumber(senderBalance.value).div(format);
let { value, valueError } = this;
let totalEth = gasTotal;
@ -428,7 +426,6 @@ export default class TransferStore {
send () {
const { options, values } = this._getTransferParams();
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
return this._getTransferMethod().postTransaction(options, values);
@ -440,16 +437,7 @@ export default class TransferStore {
}
estimateGas () {
if (this.isEth || !this.isWallet) {
return this._estimateGas();
}
return Promise
.all([
this._estimateGas(true),
this._estimateGas()
])
.then((results) => results[0].plus(results[1]));
return this._estimateGas();
}
_getTransferMethod (gas = false, forceToken = false) {

View File

@ -36,7 +36,7 @@ class WalletSettings extends Component {
};
static propTypes = {
accounts: PropTypes.object.isRequired,
accountsInfo: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
senders: PropTypes.object.isRequired
@ -113,7 +113,7 @@ class WalletSettings extends Component {
default:
case 'EDIT':
const { wallet, errors } = this.store;
const { accounts, senders } = this.props;
const { accountsInfo, senders } = this.props;
return (
<Form>
@ -137,7 +137,7 @@ class WalletSettings extends Component {
label='other wallet owners'
value={ wallet.owners.slice() }
onChange={ this.store.onOwnersChange }
accounts={ accounts }
accounts={ accountsInfo }
param='address[]'
/>
@ -190,7 +190,7 @@ class WalletSettings extends Component {
}
renderChange (change) {
const { accounts } = this.props;
const { accountsInfo } = this.props;
switch (change.type) {
case 'dailylimit':
@ -229,7 +229,7 @@ class WalletSettings extends Component {
<InputAddress
disabled
value={ change.value }
accounts={ accounts }
accounts={ accountsInfo }
/>
</div>
</div>
@ -243,7 +243,7 @@ class WalletSettings extends Component {
<InputAddress
disabled
value={ change.value }
accounts={ accounts }
accounts={ accountsInfo }
/>
</div>
</div>
@ -329,7 +329,7 @@ function mapStateToProps (initState, initProps) {
const senders = pick(accounts, owners);
return () => {
return { accounts: accountsInfo, senders };
return { accountsInfo, senders };
};
}

View File

@ -28,6 +28,8 @@ const STEPS = {
};
export default class WalletSettingsStore {
accounts = {};
@observable step = null;
@observable requests = [];
@observable deployState = '';

View File

@ -16,6 +16,7 @@
import { newError } from '~/ui/Errors/actions';
import { setAddressImage } from './providers/imagesActions';
import { openSnackbar, showSnackbar } from './providers/snackbarActions';
import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions';
import { toggleView } from '~/views/Settings/actions';
@ -23,6 +24,8 @@ export {
newError,
clearStatusLogs,
setAddressImage,
openSnackbar,
showSnackbar,
toggleStatusLogs,
toggleStatusRefresh,
toggleView

View File

@ -22,6 +22,7 @@ import SignerMiddleware from './providers/signerMiddleware';
import statusMiddleware from '~/views/Status/middleware';
import CertificationsMiddleware from './providers/certifications/middleware';
import ChainMiddleware from './providers/chainMiddleware';
export default function (api, browserHistory) {
const errors = new ErrorsMiddleware();
@ -30,12 +31,14 @@ export default function (api, browserHistory) {
const status = statusMiddleware();
const certifications = new CertificationsMiddleware();
const routeMiddleware = routerMiddleware(browserHistory);
const chain = new ChainMiddleware();
const middleware = [
settings.toMiddleware(),
signer.toMiddleware(),
errors.toMiddleware(),
certifications.toMiddleware()
certifications.toMiddleware(),
chain.toMiddleware()
];
return middleware.concat(status, routeMiddleware, thunk);

View File

@ -173,17 +173,18 @@ export function fetchTokens (_tokenIds) {
export function fetchBalances (_addresses) {
return (dispatch, getState) => {
const { api, personal } = getState();
const { visibleAccounts, accounts } = personal;
const { visibleAccounts, accountsInfo } = personal;
const addresses = uniq(_addresses || visibleAccounts || []);
const addresses = uniq((_addresses || visibleAccounts || []).concat(Object.keys(accountsInfo)));
if (addresses.length === 0) {
return Promise.resolve();
}
// With only a single account, more info will be displayed.
const fullFetch = addresses.length === 1;
const addressesToFetch = uniq(addresses.concat(Object.keys(accounts)));
const addressesToFetch = uniq(addresses);
return Promise
.all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch)))

View File

@ -0,0 +1,38 @@
// 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 { showSnackbar } from './snackbarActions';
export default class ChainMiddleware {
toMiddleware () {
return (store) => (next) => (action) => {
if (action.type === 'statusCollection') {
const { collection } = action;
if (collection && collection.netChain) {
const chain = collection.netChain;
const { nodeStatus } = store.getState();
if (chain !== nodeStatus.netChain) {
store.dispatch(showSnackbar(`Switched to ${chain}. Please reload the page.`, 5000));
}
}
}
next(action);
};
}
}

View File

@ -14,14 +14,18 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isEqual } from 'lodash';
import { isEqual, intersection } from 'lodash';
import { fetchBalances } from './balancesActions';
import { attachWallets } from './walletActions';
import Contract from '~/api/contract';
import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore';
import WalletsUtils from '~/util/wallets';
import { wallet as WalletAbi } from '~/contracts/abi';
export function personalAccountsInfo (accountsInfo) {
const addresses = [];
const accounts = {};
const contacts = {};
const contracts = {};
@ -32,6 +36,7 @@ export function personalAccountsInfo (accountsInfo) {
.filter((account) => account.uuid || !account.meta.deleted)
.forEach((account) => {
if (account.uuid) {
addresses.push(account.address);
accounts[account.address] = account;
} else if (account.meta.wallet) {
account.wallet = true;
@ -46,14 +51,52 @@ export function personalAccountsInfo (accountsInfo) {
// Load user contracts for Method Decoding
MethodDecodingStore.loadContracts(contracts);
return (dispatch) => {
const data = {
accountsInfo,
accounts, contacts, contracts, wallets
};
return (dispatch, getState) => {
const { api } = getState();
dispatch(_personalAccountsInfo(data));
dispatch(attachWallets(wallets));
const _fetchOwners = Object
.values(wallets)
.map((wallet) => {
const walletContract = new Contract(api, WalletAbi);
return WalletsUtils.fetchOwners(walletContract.at(wallet.address));
});
Promise
.all(_fetchOwners)
.then((walletsOwners) => {
return Object
.values(wallets)
.map((wallet, index) => {
wallet.owners = walletsOwners[index].map((owner) => ({
address: owner,
name: accountsInfo[owner] && accountsInfo[owner].name || owner
}));
return wallet;
});
})
.then((_wallets) => {
_wallets.forEach((wallet) => {
const owners = wallet.owners.map((o) => o.address);
// Owners ∩ Addresses not null : Wallet is owned
// by one of the accounts
if (intersection(owners, addresses).length > 0) {
accounts[wallet.address] = wallet;
} else {
contacts[wallet.address] = wallet;
}
});
const data = {
accountsInfo,
accounts, contacts, contracts
};
dispatch(_personalAccountsInfo(data));
dispatch(attachWallets(wallets));
dispatch(fetchBalances());
});
};
}

View File

@ -25,14 +25,13 @@ const initialState = {
hasContacts: false,
contracts: {},
hasContracts: false,
wallet: {},
hasWallets: false,
visibleAccounts: []
};
export default handleActions({
personalAccountsInfo (state, action) {
const { accountsInfo, accounts, contacts, contracts, wallets } = action;
const accountsInfo = action.accountsInfo || state.accountsInfo;
const { accounts, contacts, contracts } = action;
return Object.assign({}, state, {
accountsInfo,
@ -41,9 +40,7 @@ export default handleActions({
contacts,
hasContacts: Object.keys(contacts).length !== 0,
contracts,
hasContracts: Object.keys(contracts).length !== 0,
wallets,
hasWallets: Object.keys(wallets).length !== 0
hasContracts: Object.keys(contracts).length !== 0
});
},

View File

@ -90,7 +90,7 @@ export default handleActions({
signerSuccessRejectRequest (state, action) {
const { id } = action.payload;
const rejected = Object.assign(
state.pending.find(p => p.id === id),
state.pending.find(p => p.id === id) || { id },
{ status: 'rejected' }
);
return {

View File

@ -20,7 +20,7 @@ export function showSnackbar (message, cooldown) {
};
}
function openSnackbar (message, cooldown) {
export function openSnackbar (message, cooldown) {
return {
type: 'openSnackbar',
message, cooldown

View File

@ -52,6 +52,11 @@
}
}
.description {
font-size: 0.75em;
color: rgba(255, 255, 255, 0.5);
}
.accountInfo {
flex: 1;

View File

@ -43,7 +43,7 @@ export default class AccountCard extends Component {
const { account } = this.props;
const { copied } = this.state;
const { address, name, meta = {} } = account;
const { address, name, description, meta = {} } = account;
const displayName = (name && name.toUpperCase()) || address;
const { tags = [] } = meta;
@ -70,6 +70,7 @@ export default class AccountCard extends Component {
</div>
{ this.renderTags(tags, address) }
{ this.renderDescription(description) }
{ this.renderAddress(displayName, address) }
{ this.renderBalance(address) }
</div>
@ -77,6 +78,18 @@ export default class AccountCard extends Component {
);
}
renderDescription (description) {
if (!description) {
return null;
}
return (
<div className={ styles.description }>
<span>{ description }</span>
</div>
);
}
renderAddress (name, address) {
if (name === address) {
return null;

View File

@ -30,15 +30,22 @@ export default class Container extends Component {
compact: PropTypes.bool,
light: PropTypes.bool,
style: PropTypes.object,
tabIndex: PropTypes.number,
title: nodeOrStringProptype()
}
render () {
const { children, className, compact, light, style } = this.props;
const { children, className, compact, light, style, tabIndex } = this.props;
const classes = `${styles.container} ${light ? styles.light : ''} ${className}`;
const props = {};
if (Number.isInteger(tabIndex)) {
props.tabIndex = tabIndex;
}
return (
<div className={ classes } style={ style }>
<div className={ classes } style={ style } { ...props }>
<Card className={ compact ? styles.compact : styles.padded }>
{ this.renderTitle() }
{ children }

View File

@ -19,14 +19,17 @@ import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import keycode, { codes } from 'keycode';
import { FormattedMessage } from 'react-intl';
import { observer } from 'mobx-react';
import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline';
import AccountCard from '~/ui/AccountCard';
import InputAddress from '~/ui/Form/InputAddress';
import Portal from '~/ui/Portal';
import { nodeOrStringProptype } from '~/util/proptypes';
import { validateAddress } from '~/util/validation';
import AddressSelectStore from './addressSelectStore';
import styles from './addressSelect.css';
const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' };
@ -34,8 +37,11 @@ const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' };
// Current Form ID
let currentId = 1;
@observer
class AddressSelect extends Component {
static contextTypes = {
intl: React.PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
muiTheme: PropTypes.object.isRequired
};
@ -50,29 +56,31 @@ class AddressSelect extends Component {
contacts: PropTypes.object,
contracts: PropTypes.object,
tokens: PropTypes.object,
wallets: PropTypes.object,
// Optional props
allowCopy: PropTypes.bool,
allowInput: PropTypes.bool,
disabled: PropTypes.bool,
error: PropTypes.string,
hint: PropTypes.string,
label: PropTypes.string,
value: PropTypes.string
error: nodeOrStringProptype(),
hint: nodeOrStringProptype(),
label: nodeOrStringProptype(),
readOnly: PropTypes.bool,
value: nodeOrStringProptype()
};
static defaultProps = {
value: ''
};
store = new AddressSelectStore(this.context.api);
state = {
expanded: false,
focused: false,
focusedCat: null,
focusedItem: null,
inputFocused: false,
inputValue: '',
values: []
inputValue: ''
};
componentWillMount () {
@ -80,7 +88,7 @@ class AddressSelect extends Component {
}
componentWillReceiveProps (nextProps) {
if (this.values && this.values.length > 0) {
if (this.store.values && this.store.values.length > 0) {
return;
}
@ -88,36 +96,7 @@ class AddressSelect extends Component {
}
setValues (props = this.props) {
const { accounts = {}, contracts = {}, contacts = {}, wallets = {} } = props;
const accountsN = Object.keys(accounts).length;
const contractsN = Object.keys(contracts).length;
const contactsN = Object.keys(contacts).length;
const walletsN = Object.keys(wallets).length;
if (accountsN + contractsN + contactsN + walletsN === 0) {
return;
}
this.values = [
{
label: 'accounts',
values: [].concat(
Object.values(wallets),
Object.values(accounts)
)
},
{
label: 'contacts',
values: Object.values(contacts)
},
{
label: 'contracts',
values: Object.values(contracts)
}
].filter((cat) => cat.values.length > 0);
this.handleChange();
this.store.setValues(props);
}
render () {
@ -144,12 +123,12 @@ class AddressSelect extends Component {
renderInput () {
const { focused } = this.state;
const { accountsInfo, disabled, error, hint, label, value } = this.props;
const { accountsInfo, allowCopy, disabled, error, hint, label, readOnly, value } = this.props;
const input = (
<InputAddress
accountsInfo={ accountsInfo }
allowCopy={ false }
allowCopy={ allowCopy }
disabled={ disabled }
error={ error }
hint={ hint }
@ -162,7 +141,7 @@ class AddressSelect extends Component {
/>
);
if (disabled) {
if (disabled || readOnly) {
return input;
}
@ -175,14 +154,20 @@ class AddressSelect extends Component {
renderContent () {
const { muiTheme } = this.context;
const { hint, disabled, label } = this.props;
const { hint, disabled, label, readOnly } = this.props;
const { expanded, inputFocused } = this.state;
if (disabled) {
if (disabled || readOnly) {
return null;
}
const id = `addressSelect_${++currentId}`;
const ilHint = typeof hint === 'string' || !(hint && hint.props)
? (hint || '')
: this.context.intl.formatMessage(
hint.props,
hint.props.values || {}
);
return (
<Portal
@ -197,7 +182,7 @@ class AddressSelect extends Component {
<input
id={ id }
className={ styles.input }
placeholder={ hint }
placeholder={ ilHint }
onBlur={ this.handleInputBlur }
onFocus={ this.handleInputFocus }
@ -216,6 +201,7 @@ class AddressSelect extends Component {
</div>
{ this.renderCurrentInput() }
{ this.renderRegistryValues() }
{ this.renderAccounts() }
</Portal>
);
@ -241,8 +227,28 @@ class AddressSelect extends Component {
);
}
renderRegistryValues () {
const { registryValues } = this.store;
if (registryValues.length === 0) {
return null;
}
const accounts = registryValues
.map((registryValue, index) => {
const account = { ...registryValue, index: `${registryValue.address}_${index}` };
return this.renderAccountCard(account);
});
return (
<div>
{ accounts }
</div>
);
}
renderAccounts () {
const { values } = this.state;
const { values } = this.store;
if (values.length === 0) {
return (
@ -257,8 +263,8 @@ class AddressSelect extends Component {
);
}
const categories = values.map((category) => {
return this.renderCategory(category.label, category.values);
const categories = values.map((category, index) => {
return this.renderCategory(category, index);
});
return (
@ -268,7 +274,8 @@ class AddressSelect extends Component {
);
}
renderCategory (name, values = []) {
renderCategory (category, index) {
const { label, key, values = [] } = category;
let content;
if (values.length === 0) {
@ -292,8 +299,8 @@ class AddressSelect extends Component {
}
return (
<div className={ styles.category } key={ name }>
<div className={ styles.title }>{ name }</div>
<div className={ styles.category } key={ `${key}_${index}` }>
<div className={ styles.title }>{ label }</div>
{ content }
</div>
);
@ -306,7 +313,7 @@ class AddressSelect extends Component {
const balance = balances[address];
const account = {
...accountsInfo[address],
address, index
..._account
};
return (
@ -325,9 +332,10 @@ class AddressSelect extends Component {
this.inputRef = refId;
}
handleCustomInput = () => {
validateCustomInput = () => {
const { allowInput } = this.props;
const { inputValue, values } = this.state;
const { inputValue } = this.store;
const { values } = this.store;
// If input is HEX and allowInput === true, send it
if (allowInput && inputValue && /^(0x)?([0-9a-f])+$/i.test(inputValue)) {
@ -335,8 +343,8 @@ class AddressSelect extends Component {
}
// If only one value, select it
if (values.length === 1 && values[0].values.length === 1) {
const value = values[0].values[0];
if (values.reduce((cur, cat) => cur + cat.values.length, 0) === 1) {
const value = values.find((cat) => cat.values.length > 0).values[0];
return this.handleClick(value.address);
}
}
@ -361,7 +369,7 @@ class AddressSelect extends Component {
case 'enter':
const index = this.state.focusedItem;
if (!index) {
return this.handleCustomInput();
return this.validateCustomInput();
}
return this.handleDOMAction(`account_${index}`, 'click');
@ -408,10 +416,11 @@ class AddressSelect extends Component {
}
handleNavigation = (direction, event) => {
const { focusedItem, focusedCat, values } = this.state;
const { focusedItem, focusedCat } = this.state;
const { values } = this.store;
// Don't do anything if no values
if (values.length === 0) {
if (values.reduce((cur, cat) => cur + cat.values.length, 0) === 0) {
return event;
}
@ -423,7 +432,12 @@ class AddressSelect extends Component {
event.preventDefault();
const nextValues = values[focusedCat || 0];
const firstCat = values.findIndex((cat) => cat.values.length > 0);
const nextCat = focusedCat && values[focusedCat].values.length > 0
? focusedCat
: firstCat;
const nextValues = values[nextCat];
const nextFocus = nextValues ? nextValues.values[0] : null;
return this.focusItem(nextFocus && nextFocus.index || 1);
}
@ -457,12 +471,21 @@ class AddressSelect extends Component {
// If right: next category
if (direction === 'right') {
nextCategory = Math.min(prevCategoryIndex + 1, values.length - 1);
const categoryShift = values
.slice(prevCategoryIndex + 1, values.length)
.findIndex((cat) => cat.values.length > 0) + 1;
nextCategory = Math.min(prevCategoryIndex + categoryShift, values.length - 1);
}
// If right: previous category
if (direction === 'left') {
nextCategory = Math.max(prevCategoryIndex - 1, 0);
const categoryShift = values
.slice(0, prevCategoryIndex)
.reverse()
.findIndex((cat) => cat.values.length > 0) + 1;
nextCategory = Math.max(prevCategoryIndex - categoryShift, 0);
}
// If left or right: try to keep the horizontal index
@ -486,6 +509,10 @@ class AddressSelect extends Component {
}
handleMainBlur = () => {
if (this.props.readOnly) {
return;
}
if (window.document.hasFocus() && !this.state.expanded) {
this.closing = false;
this.setState({ focused: false });
@ -493,7 +520,7 @@ class AddressSelect extends Component {
}
handleMainFocus = () => {
if (this.state.focused) {
if (this.state.focused || this.props.readOnly) {
return;
}
@ -508,6 +535,12 @@ class AddressSelect extends Component {
}
handleFocus = () => {
const { disabled, readOnly } = this.props;
if (disabled || readOnly) {
return;
}
this.setState({ expanded: true, focusedItem: null, focusedCat: null }, () => {
window.setTimeout(() => {
this.handleDOMAction(this.inputRef, 'focus');
@ -525,43 +558,6 @@ class AddressSelect extends Component {
this.setState({ expanded: false });
}
/**
* Filter the given values based on the given
* filter
*/
filterValues = (values = [], _filter = '') => {
const filter = _filter.toLowerCase();
return values
// Remove empty accounts
.filter((a) => a)
.filter((account) => {
const address = account.address.toLowerCase();
const inAddress = address.includes(filter);
if (!account.name || inAddress) {
return inAddress;
}
const name = account.name.toLowerCase();
const inName = name.includes(filter);
const { meta = {} } = account;
if (!meta.tags || inName) {
return inName;
}
const tags = (meta.tags || []).join('');
return tags.includes(filter);
})
.sort((accA, accB) => {
const nameA = accA.name || accA.address;
const nameB = accB.name || accB.address;
return nameA.localeCompare(nameB);
});
}
handleInputBlur = () => {
this.setState({ inputFocused: false });
}
@ -572,25 +568,10 @@ class AddressSelect extends Component {
handleChange = (event = { target: {} }) => {
const { value = '' } = event.target;
let index = 0;
const values = this.values
.map((category) => {
const filteredValues = this
.filterValues(category.values, value)
.map((value) => {
index++;
return { ...value, index: parseInt(index) };
});
return {
label: category.label,
values: filteredValues
};
});
this.store.handleChange(value);
this.setState({
values,
focusedItem: null,
inputValue: value
});

View File

@ -0,0 +1,218 @@
// 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 from 'react';
import { observable, action } from 'mobx';
import { flatMap } from 'lodash';
import { FormattedMessage } from 'react-intl';
import Contracts from '~/contracts';
import { sha3 } from '~/api/util/sha3';
export default class AddressSelectStore {
@observable values = [];
@observable registryValues = [];
initValues = [];
regLookups = [];
constructor (api) {
this.api = api;
const { registry } = Contracts.create(api);
registry
.getContract('emailverification')
.then((emailVerification) => {
this.regLookups.push({
lookup: (value) => {
return emailVerification
.instance
.reverse.call({}, [ sha3(value) ]);
},
describe: (value) => (
<FormattedMessage
id='addressSelect.fromEmail'
defaultMessage='Verified using email {value}'
values={ {
value
} }
/>
)
});
});
registry
.getInstance()
.then((registryInstance) => {
this.regLookups.push({
lookup: (value) => {
return registryInstance
.getAddress.call({}, [ sha3(value), 'A' ]);
},
describe: (value) => (
<FormattedMessage
id='addressSelect.fromRegistry'
defaultMessage='{value} (from registry)'
values={ {
value
} }
/>
)
});
});
}
@action setValues (props) {
const { accounts = {}, contracts = {}, contacts = {} } = props;
const accountsN = Object.keys(accounts).length;
const contractsN = Object.keys(contracts).length;
const contactsN = Object.keys(contacts).length;
if (accountsN + contractsN + contactsN === 0) {
return;
}
this.initValues = [
{
key: 'accounts',
label: (
<FormattedMessage
id='addressSelect.labels.accounts'
defaultMessage='accounts'
/>
),
values: Object.values(accounts)
},
{
key: 'contacts',
label: (
<FormattedMessage
id='addressSelect.labels.contacts'
defaultMessage='contacts'
/>
),
values: Object.values(contacts)
},
{
key: 'contracts',
label: (
<FormattedMessage
id='addressSelect.labels.contracts'
defaultMessage='contracts'
/>
),
values: Object.values(contracts)
}
].filter((cat) => cat.values.length > 0);
this.handleChange();
}
@action handleChange = (value = '') => {
let index = 0;
this.values = this.initValues
.map((category) => {
const filteredValues = this
.filterValues(category.values, value)
.map((value) => {
index++;
return {
index: parseInt(index),
...value
};
});
return {
label: category.label,
values: filteredValues
};
});
// Registries Lookup
this.registryValues = [];
const lookups = this.regLookups.map((regLookup) => regLookup.lookup(value));
Promise
.all(lookups)
.then((results) => {
return results
.map((result, index) => {
if (/^(0x)?0*$/.test(result)) {
return;
}
const lowercaseResult = result.toLowerCase();
const account = flatMap(this.initValues, (cat) => cat.values)
.find((account) => account.address.toLowerCase() === lowercaseResult);
return {
description: this.regLookups[index].describe(value),
address: result,
name: account && account.name || value
};
})
.filter((data) => data);
})
.then((registryValues) => {
this.registryValues = registryValues;
});
}
/**
* Filter the given values based on the given
* filter
*/
filterValues = (values = [], _filter = '') => {
const filter = _filter.toLowerCase();
return values
// Remove empty accounts
.filter((a) => a)
.filter((account) => {
const address = account.address.toLowerCase();
const inAddress = address.includes(filter);
if (!account.name || inAddress) {
return inAddress;
}
const name = account.name.toLowerCase();
const inName = name.includes(filter);
const { meta = {} } = account;
if (!meta.tags || inName) {
return inName;
}
const tags = (meta.tags || []).join('');
return tags.includes(filter);
})
.sort((accA, accB) => {
const nameA = accA.name || accA.address;
const nameB = accB.name || accB.address;
return nameA.localeCompare(nameB);
});
}
}

View File

@ -51,6 +51,7 @@ export default class Input extends Component {
PropTypes.string,
PropTypes.bool
]),
autoFocus: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
disabled: PropTypes.bool,
@ -112,7 +113,7 @@ export default class Input extends Component {
render () {
const { value } = this.state;
const { children, className, hideUnderline, disabled, error, focused, label } = this.props;
const { autoFocus, children, className, hideUnderline, disabled, error, focused, label } = this.props;
const { hint, onClick, onFocus, multiLine, rows, type, min, max, style, tabIndex } = this.props;
const readOnly = this.props.readOnly || disabled;
@ -138,6 +139,7 @@ export default class Input extends Component {
{ this.renderCopyButton() }
<TextField
autoComplete='off'
autoFocus={ autoFocus }
className={ className }
errorText={ error }
floatingLabelFixed
@ -183,7 +185,7 @@ export default class Input extends Component {
const text = typeof allowCopy === 'string'
? allowCopy
: value;
: value.toString();
const style = hideUnderline
? {}

View File

@ -78,7 +78,7 @@ class InputAddress extends Component {
return (
<div className={ containerClasses.join(' ') }>
<Input
allowCopy={ allowCopy && (disabled ? value : false) }
allowCopy={ allowCopy && ((disabled || readOnly) ? value : false) }
className={ classes.join(' ') }
disabled={ disabled }
error={ error }
@ -103,13 +103,13 @@ class InputAddress extends Component {
}
renderIcon () {
const { value, disabled, label, allowCopy, hideUnderline } = this.props;
const { value, disabled, label, allowCopy, hideUnderline, readOnly } = this.props;
if (!value || !value.length || !util.isAddressValid(value)) {
return null;
}
const classes = [disabled ? styles.iconDisabled : styles.icon];
const classes = [(disabled || readOnly) ? styles.iconDisabled : styles.icon];
if (!label) {
classes.push(styles.noLabel);

View File

@ -25,41 +25,44 @@ class InputAddressSelect extends Component {
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired,
contracts: PropTypes.object.isRequired,
wallets: PropTypes.object.isRequired,
allowCopy: PropTypes.bool,
error: PropTypes.string,
label: PropTypes.string,
hint: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func
label: PropTypes.string,
onChange: PropTypes.func,
readOnly: PropTypes.bool,
value: PropTypes.string
};
render () {
const { accounts, contacts, contracts, wallets, label, hint, error, value, onChange } = this.props;
const { accounts, allowCopy, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
return (
<AddressSelect
allowCopy={ allowCopy }
allowInput
accounts={ accounts }
contacts={ contacts }
contracts={ contracts }
wallets={ wallets }
error={ error }
label={ label }
hint={ hint }
label={ label }
onChange={ onChange }
readOnly={ readOnly }
value={ value }
onChange={ onChange } />
/>
);
}
}
function mapStateToProps (state) {
const { accounts, contacts, contracts, wallets } = state.personal;
const { accounts, contacts, contracts } = state.personal;
return {
accounts,
contacts,
contracts,
wallets
contracts
};
}

View File

@ -14,19 +14,18 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import React, { Component, PropTypes } from 'react';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import { arrayOrObjectProptype } from '~/util/proptypes';
import styles from './radioButtons.css';
export default class RadioButtons extends Component {
static propTypes = {
name: PropTypes.string,
onChange: PropTypes.func.isRequired,
values: PropTypes.array.isRequired,
value: PropTypes.any,
name: PropTypes.string
values: arrayOrObjectProptype().isRequired
};
static defaultProps = {
@ -40,16 +39,16 @@ export default class RadioButtons extends Component {
const index = Number.isNaN(parseInt(value))
? values.findIndex((val) => val.key === value)
: parseInt(value);
const selectedValue = typeof value !== 'object' ? values[index] : value;
const selectedValue = typeof value !== 'object'
? values[index]
: value;
const key = this.getKey(selectedValue, index);
return (
<RadioButtonGroup
valueSelected={ key }
name={ name }
onChange={ this.onChange }
>
valueSelected={ key } >
{ this.renderContent() }
</RadioButtonGroup>
);
@ -59,7 +58,9 @@ export default class RadioButtons extends Component {
const { values } = this.props;
return values.map((value, index) => {
const label = typeof value === 'string' ? value : value.label || '';
const label = typeof value === 'string'
? value
: value.label || '';
const description = (typeof value !== 'string' && value.description) || null;
const key = this.getKey(value, index);
@ -67,28 +68,26 @@ export default class RadioButtons extends Component {
<RadioButton
className={ styles.spaced }
key={ index }
value={ key }
label={ (
label={
<div className={ styles.typeContainer }>
<span>{ label }</span>
{
description
? (
<span className={ styles.desc }>{ description }</span>
)
? <span className={ styles.desc }>{ description }</span>
: null
}
</div>
) }
/>
}
value={ key } />
);
});
}
getKey (value, index) {
if (typeof value !== 'string') {
return typeof value.key === 'undefined' ? index : value.key;
return typeof value.key === 'undefined'
? index
: value.key;
}
return index;
@ -96,8 +95,8 @@ export default class RadioButtons extends Component {
onChange = (event, index) => {
const { onChange, values } = this.props;
const value = values[index] || values.find((v) => v.key === index);
onChange(value, index);
}
}

View File

@ -17,6 +17,7 @@
import React, { Component, PropTypes } from 'react';
import { MenuItem, Toggle } from 'material-ui';
import { range } from 'lodash';
import BigNumber from 'bignumber.js';
import IconButton from 'material-ui/IconButton';
import AddIcon from 'material-ui/svg-icons/content/add';
@ -33,26 +34,31 @@ import styles from './typedInput.css';
export default class TypedInput extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
param: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string
]).isRequired,
accounts: PropTypes.object,
allowCopy: PropTypes.bool,
error: PropTypes.any,
hint: PropTypes.string,
isEth: PropTypes.bool,
label: PropTypes.string,
max: PropTypes.number,
min: PropTypes.number,
param: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string
]).isRequired,
onChange: PropTypes.func,
readOnly: PropTypes.bool,
value: PropTypes.any
};
static defaultProps = {
allowCopy: false,
isEth: null,
min: null,
max: null,
isEth: null
onChange: () => {},
readOnly: false
};
state = {
@ -61,21 +67,16 @@ export default class TypedInput extends Component {
};
componentWillMount () {
if (this.props.isEth && this.props.value) {
this.setState({ isEth: true, ethValue: fromWei(this.props.value) });
const { isEth, value } = this.props;
if (typeof isEth === 'boolean' && value) {
const ethValue = isEth ? fromWei(value) : value;
this.setState({ isEth, ethValue });
}
}
render () {
const { param } = this.props;
if (typeof param === 'string') {
const parsedParam = parseAbiType(param);
if (parsedParam) {
return this.renderParam(parsedParam);
}
}
const param = this.getParam();
if (param) {
return this.renderParam(param);
@ -86,7 +87,7 @@ export default class TypedInput extends Component {
}
renderParam (param) {
const { isEth } = this.props;
const { allowCopy, isEth, readOnly } = this.props;
const { type } = param;
if (type === ABI_TYPES.ARRAY) {
@ -104,10 +105,12 @@ export default class TypedInput extends Component {
return (
<TypedInput
accounts={ accounts }
allowCopy={ allowCopy }
key={ `${subtype.type}_${index}` }
onChange={ onChange }
accounts={ accounts }
param={ subtype }
readOnly={ readOnly }
value={ value[index] }
/>
);
@ -234,20 +237,23 @@ export default class TypedInput extends Component {
}
renderInteger (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props;
const { allowCopy, label, error, hint, min, max, readOnly } = this.props;
const param = this.getParam();
const realValue = value && typeof value.toNumber === 'function'
? value.toNumber()
const realValue = value
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
: value;
return (
<Input
allowCopy={ allowCopy }
label={ label }
hint={ hint }
value={ realValue }
error={ error }
onChange={ onChange }
type='number'
readOnly={ readOnly }
type={ readOnly ? 'text' : 'number' }
step={ 1 }
min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null }
@ -263,19 +269,22 @@ export default class TypedInput extends Component {
* @see https://github.com/facebook/react/issues/1549
*/
renderFloat (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props;
const { allowCopy, label, error, hint, min, max, readOnly } = this.props;
const param = this.getParam();
const realValue = value && typeof value.toNumber === 'function'
? value.toNumber()
const realValue = value
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
: value;
return (
<Input
allowCopy={ allowCopy }
label={ label }
hint={ hint }
value={ realValue }
error={ error }
onChange={ onChange }
readOnly={ readOnly }
type='text'
min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null }
@ -284,37 +293,44 @@ export default class TypedInput extends Component {
}
renderDefault () {
const { label, value, error, hint } = this.props;
const { allowCopy, label, value, error, hint, readOnly } = this.props;
return (
<Input
allowCopy={ allowCopy }
label={ label }
hint={ hint }
value={ value }
error={ error }
onSubmit={ this.onSubmit }
readOnly={ readOnly }
/>
);
}
renderAddress () {
const { accounts, label, value, error, hint } = this.props;
const { accounts, allowCopy, label, value, error, hint, readOnly } = this.props;
return (
<InputAddressSelect
allowCopy={ allowCopy }
accounts={ accounts }
label={ label }
hint={ hint }
value={ value }
error={ error }
hint={ hint }
label={ label }
onChange={ this.onChange }
editing
readOnly={ readOnly }
value={ value }
/>
);
}
renderBoolean () {
const { label, value, error, hint } = this.props;
const { allowCopy, label, value, error, hint, readOnly } = this.props;
if (readOnly) {
return this.renderDefault();
}
const boolitems = ['false', 'true'].map((bool) => {
return (
@ -329,6 +345,7 @@ export default class TypedInput extends Component {
return (
<Select
allowCopy={ allowCopy }
error={ error }
hint={ hint }
label={ label }
@ -379,7 +396,9 @@ export default class TypedInput extends Component {
}
onAddField = () => {
const { value, onChange, param } = this.props;
const { value, onChange } = this.props;
const param = this.getParam();
const newValues = [].concat(value, param.subtype.default);
onChange(newValues);
@ -392,4 +411,14 @@ export default class TypedInput extends Component {
onChange(newValues);
}
getParam = () => {
const { param } = this.props;
if (typeof param === 'string') {
return parseAbiType(param);
}
return param;
}
}

View File

@ -24,6 +24,7 @@ import LockedIcon from 'material-ui/svg-icons/action/lock-outline';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import SaveIcon from 'material-ui/svg-icons/content/save';
import SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
@ -38,6 +39,7 @@ export {
NextIcon,
PrevIcon,
SaveIcon,
SendIcon,
SnoozeIcon,
VisibleIcon
};

View File

@ -118,6 +118,15 @@ export default class MethodDecodingStore {
return Promise.resolve(result);
}
try {
const { signature } = this.api.util.decodeCallData(input);
if (signature === CONTRACT_CREATE || transaction.creates) {
result.contract = true;
return Promise.resolve({ ...result, deploy: true });
}
} catch (e) {}
return this
.isContract(contractAddress || transaction.creates)
.then((isContract) => {
@ -132,7 +141,7 @@ export default class MethodDecodingStore {
result.params = paramdata;
// Contract deployment
if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
if (!signature) {
return Promise.resolve({ ...result, deploy: true });
}
@ -192,7 +201,7 @@ export default class MethodDecodingStore {
*/
isContract (contractAddress) {
// If zero address, it isn't a contract
if (/^(0x)?0*$/.test(contractAddress)) {
if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
return Promise.resolve(false);
}

View File

@ -18,6 +18,7 @@ import { Dialog } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ReactDOM from 'react-dom';
import { nodeOrStringProptype } from '~/util/proptypes';
@ -49,6 +50,14 @@ class Modal extends Component {
waiting: PropTypes.array
}
componentDidMount () {
const element = ReactDOM.findDOMNode(this.refs.dialog);
if (element) {
element.focus();
}
}
render () {
const { muiTheme } = this.context;
const { actions, busy, children, className, current, compact, settings, steps, title, visible, waiting } = this.props;
@ -85,9 +94,12 @@ class Modal extends Component {
<Container
compact={ compact }
light
ref='dialog'
style={
{ transition: 'none' }
}>
}
tabIndex={ 0 }
>
{ children }
</Container>
</Dialog>

View File

@ -15,6 +15,32 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
$left: 1.5em;
$right: $left;
$bottom: $left;
$top: 20vh;
.backOverlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(255, 255, 255, 0.25);
z-index: -10;
opacity: 0;
transform-origin: 100% 0;
transition-property: opacity, z-index;
transition-duration: 0.25s;
transition-timing-function: ease-out;
&.expanded {
opacity: 1;
z-index: 2500;
}
}
.parityBackground {
position: absolute;
top: 0;
@ -28,10 +54,10 @@
.overlay {
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
top: $top;
left: $left;
width: calc(100vw - $left - $right);
height: calc(100vh - $top - $bottom);
transform-origin: 100% 0;
transition-property: opacity, z-index;
@ -48,7 +74,7 @@
&.expanded {
opacity: 1;
z-index: 9999;
z-index: 3500;
}
}

View File

@ -16,7 +16,7 @@
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import Portal from 'react-portal';
import ReactPortal from 'react-portal';
import keycode from 'keycode';
import { CloseIcon } from '~/ui/Icons';
@ -24,7 +24,7 @@ import ParityBackground from '~/ui/ParityBackground';
import styles from './portal.css';
export default class Protal extends Component {
export default class Portal extends Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
@ -59,23 +59,28 @@ export default class Protal extends Component {
const { children, className } = this.props;
const classes = [ styles.overlay, className ];
const backClasses = [ styles.backOverlay ];
if (expanded) {
classes.push(styles.expanded);
backClasses.push(styles.expanded);
}
return (
<Portal isOpened onClose={ this.handleClose }>
<div
className={ classes.join(' ') }
onKeyDown={ this.handleKeyDown }
>
<ParityBackground className={ styles.parityBackground } />
<ReactPortal isOpened onClose={ this.handleClose }>
<div className={ backClasses.join(' ') } onClick={ this.handleClose }>
<div
className={ classes.join(' ') }
onClick={ this.stopEvent }
onKeyDown={ this.handleKeyDown }
>
<ParityBackground className={ styles.parityBackground } />
{ this.renderCloseIcon() }
{ children }
{ this.renderCloseIcon() }
{ children }
</div>
</div>
</Portal>
</ReactPortal>
);
}
@ -93,6 +98,11 @@ export default class Protal extends Component {
);
}
stopEvent = (event) => {
event.preventDefault();
event.stopPropagation();
}
handleClose = () => {
this.props.onClose();
}

View File

@ -16,13 +16,15 @@
import React, { Component, PropTypes } from 'react';
import { arrayOrObjectProptype } from '~/util/proptypes';
import styles from './tags.css';
export default class Tags extends Component {
static propTypes = {
handleAddSearchToken: PropTypes.func,
setRefs: PropTypes.func,
tags: PropTypes.array
tags: arrayOrObjectProptype()
}
render () {

View File

@ -134,7 +134,7 @@ class TxHash extends Component {
const { api } = this.context;
const { hash } = this.props;
if (error) {
if (error || !hash || /^(0x)?0*$/.test(hash)) {
return;
}

View File

@ -16,6 +16,13 @@
import { PropTypes } from 'react';
export function arrayOrObjectProptype () {
return PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
]);
}
export function nullableProptype (type) {
return PropTypes.oneOfType([
PropTypes.oneOf([ null ]),

View File

@ -14,10 +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 WalletsUtils from '~/util/wallets';
const isValidReceipt = (receipt) => {
return receipt && receipt.blockNumber && receipt.blockNumber.gt(0);
};
function getTxArgs (func, options, values = []) {
const { contract } = func;
const { api } = contract;
const address = options.from;
if (!address) {
return Promise.resolve({ func, options, values });
}
return WalletsUtils
.isWallet(api, address)
.then((isWallet) => {
if (!isWallet) {
return { func, options, values };
}
options.data = contract.getCallData(func, options, values);
options.to = options.to || contract.address;
if (!options.to) {
return { func, options, values };
}
return WalletsUtils
.getCallArgs(api, options, values)
.then((callArgs) => {
if (!callArgs) {
return { func, options, values };
}
return callArgs;
});
});
}
export function estimateGas (_func, _options, _values = []) {
return getTxArgs(_func, _options, _values)
.then((callArgs) => {
const { func, options, values } = callArgs;
return func._estimateGas(options, values);
})
.then((gas) => {
return WalletsUtils
.isWallet(_func.contract.api, _options.from)
.then((isWallet) => {
if (isWallet) {
return gas.mul(1.5);
}
return gas;
});
});
}
export function postTransaction (_func, _options, _values = []) {
return getTxArgs(_func, _options, _values)
.then((callArgs) => {
const { func, options, values } = callArgs;
return func._postTransaction(options, values);
});
}
export function patchApi (api) {
api.patch = {
...api.patch,
contract: patchContract
};
}
export function patchContract (contract) {
contract._functions.forEach((func) => {
if (!func.constant) {
func._postTransaction = func.postTransaction;
func._estimateGas = func.estimateGas;
func.postTransaction = postTransaction.bind(contract, func);
func.estimateGas = estimateGas.bind(contract, func);
}
});
}
export function checkIfTxFailed (api, tx, gasSent) {
return api.pollMethod('eth_getTransactionReceipt', tx)
.then((receipt) => {

View File

@ -35,21 +35,21 @@ export const ERRORS = {
gasBlockLimit: 'the transaction execution will exceed the block gas limit'
};
export function validateAbi (abi, api) {
export function validateAbi (abi) {
let abiError = null;
let abiParsed = null;
try {
abiParsed = JSON.parse(abi);
if (!api.util.isArray(abiParsed)) {
if (!util.isArray(abiParsed)) {
abiError = ERRORS.invalidAbi;
return { abi, abiError, abiParsed };
}
// Validate each elements of the Array
const invalidIndex = abiParsed
.map((o) => isValidAbiEvent(o, api) || isValidAbiFunction(o, api) || isAbiFallback(o))
.map((o) => isValidAbiEvent(o) || isValidAbiFunction(o) || isAbiFallback(o))
.findIndex((valid) => !valid);
if (invalidIndex !== -1) {
@ -70,13 +70,13 @@ export function validateAbi (abi, api) {
};
}
function isValidAbiFunction (object, api) {
function isValidAbiFunction (object) {
if (!object) {
return false;
}
return ((object.type === 'function' && object.name) || object.type === 'constructor') &&
(object.inputs && api.util.isArray(object.inputs));
(object.inputs && util.isArray(object.inputs));
}
function isAbiFallback (object) {
@ -87,14 +87,14 @@ function isAbiFallback (object) {
return object.type === 'fallback';
}
function isValidAbiEvent (object, api) {
function isValidAbiEvent (object) {
if (!object) {
return false;
}
return (object.type === 'event') &&
(object.name) &&
(object.inputs && api.util.isArray(object.inputs));
(object.inputs && util.isArray(object.inputs));
}
export function validateAddress (address) {

View File

@ -14,13 +14,92 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { range, uniq } from 'lodash';
import BigNumber from 'bignumber.js';
import { intersection, range, uniq } from 'lodash';
import Contract from '~/api/contract';
import { bytesToHex, toHex } from '~/api/util/format';
import { validateAddress } from '~/util/validation';
import WalletAbi from '~/contracts/abi/wallet.json';
const _cachedWalletLookup = {};
export default class WalletsUtils {
static getCallArgs (api, options, values = []) {
const walletContract = new Contract(api, WalletAbi);
const promises = [
api.parity.accountsInfo(),
WalletsUtils.fetchOwners(walletContract.at(options.from))
];
return Promise
.all(promises)
.then(([ accounts, owners ]) => {
const addresses = Object.keys(accounts);
const owner = intersection(addresses, owners).pop();
if (!owner) {
return false;
}
return owner;
})
.then((owner) => {
if (!owner) {
return false;
}
const _options = Object.assign({}, options);
const { from, to, value = new BigNumber(0), data } = options;
delete _options.data;
const nextValues = [ to, value, data ];
const nextOptions = {
..._options,
from: owner,
to: from,
value: new BigNumber(0)
};
const execFunc = walletContract.instance.execute;
return { func: execFunc, options: nextOptions, values: nextValues };
});
}
/**
* Check whether the given address could be
* a Wallet. The result is cached in order not
* to make unnecessary calls on non-wallet accounts
*/
static isWallet (api, address) {
if (!_cachedWalletLookup[address]) {
const walletContract = new Contract(api, WalletAbi);
_cachedWalletLookup[address] = walletContract
.at(address)
.instance
.m_numOwners
.call()
.then((result) => {
if (!result || result.equals(0)) {
return false;
}
return true;
})
.then((bool) => {
_cachedWalletLookup[address] = Promise.resolve(bool);
return bool;
});
}
return _cachedWalletLookup[address];
}
static fetchRequire (walletContract) {
return walletContract.instance.m_required.call();
}

View File

@ -42,7 +42,6 @@ export default class Header extends Component {
render () {
const { account, balance, className, children, hideName } = this.props;
const { address, meta, uuid } = account;
if (!account) {
return null;
}

View File

@ -34,7 +34,6 @@ class List extends Component {
order: PropTypes.string,
orderFallback: PropTypes.string,
search: PropTypes.array,
walletsOwners: PropTypes.object,
fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired,
@ -58,7 +57,7 @@ class List extends Component {
}
renderAccounts () {
const { accounts, balances, empty, link, walletsOwners, handleAddSearchToken } = this.props;
const { accounts, balances, empty, link, handleAddSearchToken } = this.props;
if (empty) {
return (
@ -76,7 +75,7 @@ class List extends Component {
const account = accounts[address] || {};
const balance = balances[address] || {};
const owners = walletsOwners && walletsOwners[address] || null;
const owners = account.owners || null;
return (
<div

View File

@ -157,7 +157,11 @@ export default class Summary extends Component {
const { link, noLink, account, name } = this.props;
const { address } = account;
const viewLink = `/${link || 'accounts'}/${address}`;
const baseLink = account.wallet
? 'wallet'
: link || 'accounts';
const viewLink = `/${baseLink}/${address}`;
const content = (
<IdentityName address={ address } name={ name } unknown />

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ContentAdd from 'material-ui/svg-icons/content/add';
import { uniq, isEqual } from 'lodash';
import { uniq, isEqual, pickBy, omitBy } from 'lodash';
import List from './List';
import { CreateAccount, CreateWallet } from '~/modals';
@ -36,9 +36,6 @@ class Accounts extends Component {
setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object.isRequired,
hasAccounts: PropTypes.bool.isRequired,
wallets: PropTypes.object.isRequired,
walletsOwners: PropTypes.object.isRequired,
hasWallets: PropTypes.bool.isRequired,
balances: PropTypes.object
}
@ -62,8 +59,8 @@ class Accounts extends Component {
}
componentWillReceiveProps (nextProps) {
const prevAddresses = Object.keys({ ...this.props.accounts, ...this.props.wallets });
const nextAddresses = Object.keys({ ...nextProps.accounts, ...nextProps.wallets });
const prevAddresses = Object.keys(this.props.accounts);
const nextAddresses = Object.keys(nextProps.accounts);
if (prevAddresses.length !== nextAddresses.length || !isEqual(prevAddresses.sort(), nextAddresses.sort())) {
this.setVisibleAccounts(nextProps);
@ -75,8 +72,8 @@ class Accounts extends Component {
}
setVisibleAccounts (props = this.props) {
const { accounts, wallets, setVisibleAccounts } = props;
const addresses = Object.keys({ ...accounts, ...wallets });
const { accounts, setVisibleAccounts } = props;
const addresses = Object.keys(accounts);
setVisibleAccounts(addresses);
}
@ -115,30 +112,38 @@ class Accounts extends Component {
}
renderAccounts () {
const { accounts, balances } = this.props;
const _accounts = omitBy(accounts, (a) => a.wallet);
const _hasAccounts = Object.keys(_accounts).length > 0;
if (!this.state.show) {
return this.renderLoading(this.props.accounts);
return this.renderLoading(_accounts);
}
const { accounts, hasAccounts, balances } = this.props;
const { searchValues, sortOrder } = this.state;
return (
<List
search={ searchValues }
accounts={ accounts }
accounts={ _accounts }
balances={ balances }
empty={ !hasAccounts }
empty={ !_hasAccounts }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } />
);
}
renderWallets () {
const { accounts, balances } = this.props;
const wallets = pickBy(accounts, (a) => a.wallet);
const hasWallets = Object.keys(wallets).length > 0;
if (!this.state.show) {
return this.renderLoading(this.props.wallets);
return this.renderLoading(wallets);
}
const { wallets, hasWallets, balances, walletsOwners } = this.props;
const { searchValues, sortOrder } = this.state;
if (!wallets || Object.keys(wallets).length === 0) {
@ -154,7 +159,6 @@ class Accounts extends Component {
empty={ !hasWallets }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken }
walletsOwners={ walletsOwners }
/>
);
}
@ -287,34 +291,12 @@ class Accounts extends Component {
}
function mapStateToProps (state) {
const { accounts, hasAccounts, wallets, hasWallets, accountsInfo } = state.personal;
const { accounts, hasAccounts } = state.personal;
const { balances } = state.balances;
const walletsInfo = state.wallet.wallets;
const walletsOwners = Object
.keys(walletsInfo)
.map((wallet) => {
const owners = walletsInfo[wallet].owners || [];
return {
owners: owners.map((owner) => ({
address: owner,
name: accountsInfo[owner] && accountsInfo[owner].name || owner
})),
address: wallet
};
})
.reduce((walletsOwners, wallet) => {
walletsOwners[wallet.address] = wallet.owners;
return walletsOwners;
}, {});
return {
accounts,
hasAccounts,
wallets,
walletsOwners,
hasWallets,
accounts: accounts,
hasAccounts: hasAccounts,
balances
};
}

View File

@ -19,29 +19,31 @@ import React, { Component, PropTypes } from 'react';
import LinearProgress from 'material-ui/LinearProgress';
import { Card, CardActions, CardTitle, CardText } from 'material-ui/Card';
import { Button, Input, InputAddress, InputAddressSelect } from '~/ui';
import { Button, TypedInput } from '~/ui';
import { arrayOrObjectProptype } from '~/util/proptypes';
import styles from './queries.css';
export default class InputQuery extends Component {
static contextTypes = {
api: PropTypes.object
}
};
static propTypes = {
accountsInfo: PropTypes.object.isRequired,
contract: PropTypes.object.isRequired,
inputs: PropTypes.array.isRequired,
outputs: PropTypes.array.isRequired,
inputs: arrayOrObjectProptype().isRequired,
outputs: arrayOrObjectProptype().isRequired,
name: PropTypes.string.isRequired,
signature: PropTypes.string.isRequired,
className: PropTypes.string
}
};
state = {
isValid: true,
results: [],
values: {}
}
};
render () {
const { name, className } = this.props;
@ -89,7 +91,7 @@ export default class InputQuery extends Component {
renderResults () {
const { results, isLoading } = this.state;
const { outputs } = this.props;
const { accountsInfo, outputs } = this.props;
if (isLoading) {
return (<LinearProgress mode='indeterminate' />);
@ -108,25 +110,16 @@ export default class InputQuery extends Component {
}))
.sort((outA, outB) => outA.display.length - outB.display.length)
.map((out, index) => {
let input = null;
if (out.type === 'address') {
input = (
<InputAddress
className={ styles.queryValue }
disabled
value={ out.display }
/>
);
} else {
input = (
<Input
className={ styles.queryValue }
readOnly
allowCopy
value={ out.display }
/>
);
}
const input = (
<TypedInput
accounts={ accountsInfo }
allowCopy
isEth={ false }
param={ out.type }
readOnly
value={ out.display }
/>
);
return (
<div key={ index }>
@ -144,8 +137,7 @@ export default class InputQuery extends Component {
const { name, type } = input;
const label = `${name ? `${name}: ` : ''}${type}`;
const onChange = (event, input) => {
const value = event && event.target.value || input;
const onChange = (value) => {
const { values } = this.state;
this.setState({
@ -156,28 +148,15 @@ export default class InputQuery extends Component {
});
};
if (type === 'address') {
return (
<div key={ name }>
<InputAddressSelect
hint={ type }
label={ label }
value={ values[name] }
required
onChange={ onChange }
/>
</div>
);
}
return (
<div key={ name }>
<Input
<TypedInput
hint={ type }
label={ label }
value={ values[name] }
required
isEth={ false }
onChange={ onChange }
param={ type }
value={ values[name] }
/>
</div>
);
@ -192,7 +171,9 @@ export default class InputQuery extends Component {
if (api.util.isInstanceOf(value, BigNumber)) {
return value.toFormat(0);
} else if (api.util.isArray(value)) {
}
if (api.util.isArray(value)) {
return api.util.bytesToHex(value);
}

View File

@ -14,12 +14,11 @@
// 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 React, { Component, PropTypes } from 'react';
import { Card, CardTitle, CardText } from 'material-ui/Card';
import InputQuery from './inputQuery';
import { Container, Input, InputAddress } from '~/ui';
import { Container, TypedInput } from '~/ui';
import styles from './queries.css';
@ -29,6 +28,7 @@ export default class Queries extends Component {
}
static propTypes = {
accountsInfo: PropTypes.object.isRequired,
contract: PropTypes.object,
values: PropTypes.object
}
@ -74,11 +74,12 @@ export default class Queries extends Component {
renderInputQuery (fn) {
const { abi, name, signature } = fn;
const { contract } = this.props;
const { accountsInfo, contract } = this.props;
return (
<div className={ styles.container } key={ fn.signature }>
<InputQuery
accountsInfo={ accountsInfo }
className={ styles.method }
inputs={ abi.inputs }
outputs={ abi.outputs }
@ -116,34 +117,23 @@ export default class Queries extends Component {
}
const { api } = this.context;
let valueToDisplay = null;
const { accountsInfo } = this.props;
if (api.util.isInstanceOf(value, BigNumber)) {
valueToDisplay = value.toFormat(0);
} else if (api.util.isArray(value)) {
let valueToDisplay = value;
if (api.util.isArray(value)) {
valueToDisplay = api.util.bytesToHex(value);
} else if (typeof value === 'boolean') {
valueToDisplay = value ? 'true' : 'false';
} else {
valueToDisplay = value.toString();
}
if (type === 'address') {
return (
<InputAddress
className={ styles.queryValue }
value={ valueToDisplay }
disabled
/>
);
}
return (
<Input
className={ styles.queryValue }
value={ valueToDisplay }
readOnly
<TypedInput
accounts={ accountsInfo }
allowCopy
isEth={ false }
param={ type }
readOnly
value={ valueToDisplay }
/>
);
}

View File

@ -46,6 +46,7 @@ class Contract extends Component {
setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object,
accountsInfo: PropTypes.object,
balances: PropTypes.object,
contracts: PropTypes.object,
isTest: PropTypes.bool,
@ -115,7 +116,7 @@ class Contract extends Component {
}
render () {
const { balances, contracts, params, isTest } = this.props;
const { accountsInfo, balances, contracts, params, isTest } = this.props;
const { allEvents, contract, queryValues, loadingEvents } = this.state;
const account = contracts[params.address];
const balance = balances[params.address];
@ -138,6 +139,7 @@ class Contract extends Component {
/>
<Queries
accountsInfo={ accountsInfo }
contract={ contract }
values={ queryValues }
/>
@ -447,13 +449,14 @@ class Contract extends Component {
}
function mapStateToProps (state) {
const { accounts, contracts } = state.personal;
const { accounts, accountsInfo, contracts } = state.personal;
const { balances } = state.balances;
const { isTest } = state.nodeStatus;
return {
isTest,
accounts,
accountsInfo,
contracts,
balances
};

View File

@ -20,6 +20,7 @@ import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { omitBy } from 'lodash';
import { AddDapps, DappPermissions } from '~/modals';
import PermissionStore from '~/modals/DappPermissions/store';
@ -150,8 +151,15 @@ class Dapps extends Component {
function mapStateToProps (state) {
const { accounts } = state.personal;
/**
* Do not show the Wallet Accounts in the Dapps
* Permissions Modal. This will come in v1.6, but
* for now it would break dApps using Web3...
*/
const _accounts = omitBy(accounts, (account) => account.wallet);
return {
accounts
accounts: _accounts
};
}

View File

@ -23,6 +23,7 @@ export default class RequestPending extends Component {
static propTypes = {
className: PropTypes.string,
date: PropTypes.instanceOf(Date).isRequired,
focus: PropTypes.bool,
gasLimit: PropTypes.object.isRequired,
id: PropTypes.object.isRequired,
isSending: PropTypes.bool.isRequired,
@ -38,6 +39,7 @@ export default class RequestPending extends Component {
};
static defaultProps = {
focus: false,
isSending: false
};
@ -49,7 +51,7 @@ export default class RequestPending extends Component {
};
render () {
const { className, date, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props;
const { className, date, focus, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props;
if (payload.sign) {
const { sign } = payload;
@ -58,6 +60,7 @@ export default class RequestPending extends Component {
<SignRequest
address={ sign.address }
className={ className }
focus={ focus }
hash={ sign.hash }
id={ id }
isFinished={ false }
@ -75,6 +78,7 @@ export default class RequestPending extends Component {
<TransactionPending
className={ className }
date={ date }
focus={ focus }
gasLimit={ gasLimit }
id={ id }
isSending={ isSending }

View File

@ -30,13 +30,19 @@ export default class SignRequest extends Component {
address: PropTypes.string.isRequired,
hash: PropTypes.string.isRequired,
isFinished: PropTypes.bool.isRequired,
isTest: PropTypes.bool.isRequired,
store: PropTypes.object.isRequired,
className: PropTypes.string,
focus: PropTypes.bool,
isSending: PropTypes.bool,
onConfirm: PropTypes.func,
onReject: PropTypes.func,
status: PropTypes.string,
className: PropTypes.string,
isTest: PropTypes.bool.isRequired,
store: PropTypes.object.isRequired
status: PropTypes.string
};
static defaultProps = {
focus: false
};
componentWillMount () {
@ -81,7 +87,7 @@ export default class SignRequest extends Component {
}
renderActions () {
const { address, isFinished, status } = this.props;
const { address, focus, isFinished, status } = this.props;
if (isFinished) {
if (status === 'confirmed') {
@ -111,6 +117,7 @@ export default class SignRequest extends Component {
return (
<TransactionPendingForm
address={ address }
focus={ focus }
isSending={ this.props.isSending }
onConfirm={ this.onConfirm }
onReject={ this.onReject }

View File

@ -35,6 +35,7 @@ export default class TransactionPending extends Component {
static propTypes = {
className: PropTypes.string,
date: PropTypes.instanceOf(Date).isRequired,
focus: PropTypes.bool,
gasLimit: PropTypes.object,
id: PropTypes.object.isRequired,
isSending: PropTypes.bool.isRequired,
@ -53,6 +54,10 @@ export default class TransactionPending extends Component {
}).isRequired
};
static defaultProps = {
focus: false
};
gasStore = new GasPriceEditor.Store(this.context.api, {
gas: this.props.transaction.gas.toFixed(),
gasLimit: this.props.gasLimit,
@ -80,7 +85,7 @@ export default class TransactionPending extends Component {
}
renderTransaction () {
const { className, id, isSending, isTest, store, transaction } = this.props;
const { className, focus, id, isSending, isTest, store, transaction } = this.props;
const { totalValue } = this.state;
const { from, value } = transaction;
@ -100,6 +105,7 @@ export default class TransactionPending extends Component {
value={ value } />
<TransactionPendingForm
address={ from }
focus={ focus }
isSending={ isSending }
onConfirm={ this.onConfirm }
onReject={ this.onReject } />

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import RaisedButton from 'material-ui/RaisedButton';
@ -26,11 +27,16 @@ import styles from './transactionPendingFormConfirm.css';
class TransactionPendingFormConfirm extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
account: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
isSending: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired
}
onConfirm: PropTypes.func.isRequired,
focus: PropTypes.bool
};
static defaultProps = {
focus: false
};
id = Math.random(); // for tooltip
@ -40,10 +46,39 @@ class TransactionPendingFormConfirm extends Component {
walletError: null
}
componentDidMount () {
this.focus();
}
componentWillReceiveProps (nextProps) {
if (!this.props.focus && nextProps.focus) {
this.focus(nextProps);
}
}
/**
* Properly focus on the input element when needed.
* This might be fixed some day in MaterialUI with
* an autoFocus prop.
*
* @see https://github.com/callemall/material-ui/issues/5632
*/
focus (props = this.props) {
if (props.focus) {
const textNode = ReactDOM.findDOMNode(this.refs.input);
if (!textNode) {
return;
}
const inputNode = textNode.querySelector('input');
inputNode && inputNode.focus();
}
}
render () {
const { accounts, address, isSending } = this.props;
const { account, address, isSending } = this.props;
const { password, wallet, walletError } = this.state;
const account = accounts[address] || {};
const isExternal = !account.uuid;
const passwordHint = account.meta && account.meta.passwordHint
@ -72,8 +107,10 @@ class TransactionPendingFormConfirm extends Component {
}
onChange={ this.onModifyPassword }
onKeyDown={ this.onKeyDown }
ref='input'
type='password'
value={ password } />
value={ password }
/>
<div className={ styles.passwordHint }>
{ passwordHint }
</div>
@ -178,11 +215,14 @@ class TransactionPendingFormConfirm extends Component {
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
function mapStateToProps (initState, initProps) {
const { accounts } = initState.personal;
const { address } = initProps;
return {
accounts
const account = accounts[address] || {};
return () => {
return { account };
};
}

View File

@ -28,7 +28,12 @@ export default class TransactionPendingForm extends Component {
isSending: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
className: PropTypes.string
className: PropTypes.string,
focus: PropTypes.bool
};
static defaultProps = {
focus: false
};
state = {
@ -47,7 +52,7 @@ export default class TransactionPendingForm extends Component {
}
renderForm () {
const { address, isSending, onConfirm, onReject } = this.props;
const { address, focus, isSending, onConfirm, onReject } = this.props;
if (this.state.isRejectOpen) {
return (
@ -59,8 +64,10 @@ export default class TransactionPendingForm extends Component {
return (
<TransactionPendingFormConfirm
address={ address }
focus={ focus }
isSending={ isSending }
onConfirm={ onConfirm } />
onConfirm={ onConfirm }
/>
);
}

View File

@ -78,7 +78,7 @@ class Embedded extends Component {
);
}
renderPending = (data) => {
renderPending = (data, index) => {
const { actions, gasLimit, isTest } = this.props;
const { date, id, isSending, payload } = data;
@ -86,6 +86,7 @@ class Embedded extends Component {
<RequestPending
className={ styles.request }
date={ date }
focus={ index === 0 }
gasLimit={ gasLimit }
id={ id }
isSending={ isSending }

View File

@ -104,7 +104,7 @@ class RequestsPage extends Component {
);
}
renderPending = (data) => {
renderPending = (data, index) => {
const { actions, gasLimit, isTest } = this.props;
const { date, id, isSending, payload } = data;
@ -112,6 +112,7 @@ class RequestsPage extends Component {
<RequestPending
className={ styles.request }
date={ date }
focus={ index === 0 }
gasLimit={ gasLimit }
id={ id }
isSending={ isSending }

View File

@ -55,14 +55,20 @@ export default class WalletDetails extends Component {
return null;
}
const ownersList = owners.map((address, idx) => (
<InputAddress
key={ `${idx}_${address}` }
value={ address }
disabled
text
/>
));
const ownersList = owners.map((owner, idx) => {
const address = typeof owner === 'object'
? owner.address
: owner;
return (
<InputAddress
key={ `${idx}_${address}` }
value={ address }
disabled
text
/>
);
});
return (
<div>

View File

@ -57,12 +57,12 @@ export default class WalletTransactions extends Component {
);
}
const txRows = transactions.map((transaction) => {
const txRows = transactions.slice(0, 15).map((transaction, index) => {
const { transactionHash, blockNumber, from, to, value, data } = transaction;
return (
<TxRow
key={ transactionHash }
key={ `${transactionHash}_${index}` }
tx={ {
hash: transactionHash,
input: data && bytesToHex(data) || '',

View File

@ -64,13 +64,14 @@ class Wallet extends Component {
};
static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
address: PropTypes.string.isRequired,
balance: nullableProptype(PropTypes.object.isRequired),
images: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
wallets: PropTypes.object.isRequired,
isTest: PropTypes.bool.isRequired,
owned: PropTypes.bool.isRequired,
setVisibleAccounts: PropTypes.func.isRequired,
wallet: PropTypes.object.isRequired,
isTest: PropTypes.bool.isRequired
walletAccount: nullableProptype(PropTypes.object).isRequired
};
state = {
@ -104,28 +105,26 @@ class Wallet extends Component {
}
render () {
const { wallets, balance, address } = this.props;
const { walletAccount, balance, wallet } = this.props;
const wallet = (wallets || {})[address];
if (!wallet) {
if (!walletAccount) {
return null;
}
const { owners, require, dailylimit } = this.props.wallet;
const { owners, require, dailylimit } = wallet;
return (
<div className={ styles.wallet }>
{ this.renderEditDialog(wallet) }
{ this.renderEditDialog(walletAccount) }
{ this.renderSettingsDialog() }
{ this.renderTransferDialog() }
{ this.renderDeleteDialog(wallet) }
{ this.renderDeleteDialog(walletAccount) }
{ this.renderActionbar() }
<Page>
<div className={ styles.info }>
<Header
className={ styles.header }
account={ wallet }
account={ walletAccount }
balance={ balance }
isContract
>
@ -209,32 +208,47 @@ class Wallet extends Component {
}
renderActionbar () {
const { balance } = this.props;
const { balance, owned } = this.props;
const showTransferButton = !!(balance && balance.tokens);
const buttons = [
<Button
key='transferFunds'
icon={ <ContentSend /> }
label='transfer'
disabled={ !showTransferButton }
onClick={ this.onTransferClick } />,
const buttons = [];
if (owned) {
buttons.push(
<Button
key='transferFunds'
icon={ <ContentSend /> }
label='transfer'
disabled={ !showTransferButton }
onClick={ this.onTransferClick } />
);
}
buttons.push(
<Button
key='delete'
icon={ <ActionDelete /> }
label='delete'
onClick={ this.showDeleteDialog } />,
onClick={ this.showDeleteDialog } />
);
buttons.push(
<Button
key='editmeta'
icon={ <ContentCreate /> }
label='edit'
onClick={ this.onEditClick } />,
<Button
key='settings'
icon={ <SettingsIcon /> }
label='settings'
onClick={ this.onSettingsClick } />
];
onClick={ this.onEditClick } />
);
if (owned) {
buttons.push(
<Button
key='settings'
icon={ <SettingsIcon /> }
label='settings'
onClick={ this.onSettingsClick } />
);
}
return (
<Actionbar
@ -293,12 +307,11 @@ class Wallet extends Component {
return null;
}
const { wallets, balance, images, address } = this.props;
const wallet = wallets[address];
const { walletAccount, balance, images } = this.props;
return (
<Transfer
account={ wallet }
account={ walletAccount }
balance={ balance }
images={ images }
onClose={ this.onTransferClose }
@ -342,20 +355,27 @@ function mapStateToProps (_, initProps) {
return (state) => {
const { isTest } = state.nodeStatus;
const { wallets } = state.personal;
const { accountsInfo = {}, accounts = {} } = state.personal;
const { balances } = state.balances;
const { images } = state;
const walletAccount = accounts[address] || accountsInfo[address] || null;
if (walletAccount) {
walletAccount.address = address;
}
const wallet = state.wallet.wallets[address] || {};
const balance = balances[address] || null;
const owned = !!accounts[address];
return {
isTest,
wallets,
address,
balance,
images,
address,
wallet
isTest,
owned,
wallet,
walletAccount
};
};
}

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
// Run with `webpack --config webpack.libraries.js --progress`
// Run with `webpack --config webpack.libraries.js`
const path = require('path');
@ -38,6 +38,13 @@ module.exports = {
library: '[name].js',
libraryTarget: 'umd'
},
resolve: {
alias: {
'~': path.resolve(__dirname, '../src')
}
},
module: {
rules: [
{

View File

@ -69,6 +69,9 @@ module.exports = {
},
resolve: {
alias: {
'~': path.resolve(__dirname, '../src')
},
modules: [
path.resolve('./src'),
path.join(__dirname, '../node_modules')

View File

@ -24,6 +24,7 @@ const postcssNested = require('postcss-nested');
const postcssVars = require('postcss-simple-vars');
const rucksack = require('rucksack-css');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const ENV = process.env.NODE_ENV || 'development';
const isProd = ENV === 'production';
@ -79,6 +80,10 @@ function getPlugins (_isProd = isProd) {
];
const plugins = [
new ProgressBarPlugin({
format: '[:msg] [:bar] ' + ':percent' + ' (:elapsed seconds)'
}),
// NB: HappyPack is not yet working with Webpack 2... (as of Nov. 26)
// new HappyPack({

View File

@ -64,6 +64,13 @@ module.exports = {
}
]
},
resolve: {
alias: {
'~': path.resolve(__dirname, '../src')
}
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../', `${DEST}/`),

View File

@ -27,7 +27,7 @@ use user_defaults::UserDefaults;
#[derive(Debug, PartialEq)]
pub enum SpecType {
Mainnet,
Testnet,
Morden,
Ropsten,
Olympic,
Classic,
@ -49,8 +49,8 @@ impl str::FromStr for SpecType {
let spec = match s {
"frontier" | "homestead" | "mainnet" => SpecType::Mainnet,
"frontier-dogmatic" | "homestead-dogmatic" | "classic" => SpecType::Classic,
"morden" | "testnet" | "classic-testnet" => SpecType::Testnet,
"ropsten" => SpecType::Ropsten,
"morden" | "classic-testnet" => SpecType::Morden,
"ropsten" | "testnet" => SpecType::Ropsten,
"olympic" => SpecType::Olympic,
"expanse" => SpecType::Expanse,
"dev" => SpecType::Dev,
@ -64,7 +64,7 @@ impl SpecType {
pub fn spec(&self) -> Result<Spec, String> {
match *self {
SpecType::Mainnet => Ok(ethereum::new_frontier()),
SpecType::Testnet => Ok(ethereum::new_morden()),
SpecType::Morden => Ok(ethereum::new_morden()),
SpecType::Ropsten => Ok(ethereum::new_ropsten()),
SpecType::Olympic => Ok(ethereum::new_olympic()),
SpecType::Classic => Ok(ethereum::new_classic()),
@ -292,12 +292,12 @@ mod tests {
assert_eq!(SpecType::Mainnet, "frontier".parse().unwrap());
assert_eq!(SpecType::Mainnet, "homestead".parse().unwrap());
assert_eq!(SpecType::Mainnet, "mainnet".parse().unwrap());
assert_eq!(SpecType::Testnet, "testnet".parse().unwrap());
assert_eq!(SpecType::Testnet, "morden".parse().unwrap());
assert_eq!(SpecType::Ropsten, "testnet".parse().unwrap());
assert_eq!(SpecType::Morden, "morden".parse().unwrap());
assert_eq!(SpecType::Ropsten, "ropsten".parse().unwrap());
assert_eq!(SpecType::Olympic, "olympic".parse().unwrap());
assert_eq!(SpecType::Classic, "classic".parse().unwrap());
assert_eq!(SpecType::Testnet, "classic-testnet".parse().unwrap());
assert_eq!(SpecType::Morden, "classic-testnet".parse().unwrap());
}
#[test]

View File

@ -193,6 +193,7 @@ fn rpc_eth_logs() {
data: vec![1,2,3],
},
transaction_index: 0,
transaction_log_index: 0,
transaction_hash: H256::default(),
log_index: 0,
}, LocalizedLogEntry {
@ -204,8 +205,9 @@ fn rpc_eth_logs() {
data: vec![1,2,3],
},
transaction_index: 0,
transaction_log_index: 1,
transaction_hash: H256::default(),
log_index: 0,
log_index: 1,
}]);
@ -213,8 +215,8 @@ fn rpc_eth_logs() {
let request2 = r#"{"jsonrpc": "2.0", "method": "eth_getLogs", "params": [{"limit":1}], "id": 1}"#;
let request3 = r#"{"jsonrpc": "2.0", "method": "eth_getLogs", "params": [{"limit":0}], "id": 1}"#;
let response1 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"},{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"id":1}"#;
let response2 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"id":1}"#;
let response1 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x0","type":"mined"},{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x1","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x1","type":"mined"}],"id":1}"#;
let response2 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x1","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x1","type":"mined"}],"id":1}"#;
let response3 = r#"{"jsonrpc":"2.0","result":[],"id":1}"#;
assert_eq!(tester.io.handle_request_sync(request1), Some(response1.to_owned()));
@ -235,6 +237,7 @@ fn rpc_logs_filter() {
data: vec![1,2,3],
},
transaction_index: 0,
transaction_log_index: 0,
transaction_hash: H256::default(),
log_index: 0,
}, LocalizedLogEntry {
@ -246,8 +249,9 @@ fn rpc_logs_filter() {
data: vec![1,2,3],
},
transaction_index: 0,
transaction_log_index: 1,
transaction_hash: H256::default(),
log_index: 0,
log_index: 1,
}]);
// Register filters first
@ -261,8 +265,8 @@ fn rpc_logs_filter() {
let request_changes1 = r#"{"jsonrpc": "2.0", "method": "eth_getFilterChanges", "params": ["0x0"], "id": 1}"#;
let request_changes2 = r#"{"jsonrpc": "2.0", "method": "eth_getFilterChanges", "params": ["0x1"], "id": 1}"#;
let response1 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"},{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"id":1}"#;
let response2 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"id":1}"#;
let response1 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x0","type":"mined"},{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x1","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x1","type":"mined"}],"id":1}"#;
let response2 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x1","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x1","type":"mined"}],"id":1}"#;
assert_eq!(tester.io.handle_request_sync(request_changes1), Some(response1.to_owned()));
assert_eq!(tester.io.handle_request_sync(request_changes2), Some(response2.to_owned()));
@ -951,6 +955,7 @@ fn rpc_eth_transaction_receipt() {
block_number: 0x4510c,
transaction_hash: H256::new(),
transaction_index: 0,
transaction_log_index: 0,
log_index: 1,
}],
log_bloom: 0.into(),
@ -967,7 +972,7 @@ fn rpc_eth_transaction_receipt() {
"params": ["0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"],
"id": 1
}"#;
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","contractAddress":null,"cumulativeGasUsed":"0x20","gasUsed":"0x10","logs":[{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","data":"0x","logIndex":"0x1","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","root":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0"},"id":1}"#;
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","contractAddress":null,"cumulativeGasUsed":"0x20","gasUsed":"0x10","logs":[{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","data":"0x","logIndex":"0x1","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x0","type":"mined"}],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","root":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0"},"id":1}"#;
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
}

View File

@ -38,9 +38,12 @@ pub struct Log {
/// Transaction Index
#[serde(rename="transactionIndex")]
pub transaction_index: Option<U256>,
/// Log Index
/// Log Index in Block
#[serde(rename="logIndex")]
pub log_index: Option<U256>,
/// Log Index in Transaction
#[serde(rename="transactionLogIndex")]
pub transaction_log_index: Option<U256>,
/// Log Type
#[serde(rename="type")]
pub log_type: String,
@ -57,6 +60,7 @@ impl From<LocalizedLogEntry> for Log {
transaction_hash: Some(e.transaction_hash.into()),
transaction_index: Some(e.transaction_index.into()),
log_index: Some(e.log_index.into()),
transaction_log_index: Some(e.transaction_log_index.into()),
log_type: "mined".to_owned(),
}
}
@ -73,6 +77,7 @@ impl From<LogEntry> for Log {
transaction_hash: None,
transaction_index: None,
log_index: None,
transaction_log_index: None,
log_type: "pending".to_owned(),
}
}
@ -86,7 +91,7 @@ mod tests {
#[test]
fn log_serialization() {
let s = r#"{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"data":"0x","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","logIndex":"0x1","type":"mined"}"#;
let s = r#"{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"data":"0x","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","logIndex":"0x1","transactionLogIndex":"0x1","type":"mined"}"#;
let log = Log {
address: H160::from_str("33990122638b9132ca29c723bdf037f1a891a70c").unwrap(),
@ -99,6 +104,7 @@ mod tests {
block_number: Some(U256::from(0x4510c)),
transaction_hash: Some(H256::default()),
transaction_index: Some(U256::default()),
transaction_log_index: Some(1.into()),
log_index: Some(U256::from(1)),
log_type: "mined".to_owned(),
};

View File

@ -109,7 +109,7 @@ mod tests {
#[test]
fn receipt_serialization() {
let s = r#"{"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","cumulativeGasUsed":"0x20","gasUsed":"0x10","contractAddress":null,"logs":[{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"data":"0x","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","logIndex":"0x1","type":"mined"}],"root":"0x000000000000000000000000000000000000000000000000000000000000000a","logsBloom":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f"}"#;
let s = r#"{"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","cumulativeGasUsed":"0x20","gasUsed":"0x10","contractAddress":null,"logs":[{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"data":"0x","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","logIndex":"0x1","transactionLogIndex":null,"type":"mined"}],"root":"0x000000000000000000000000000000000000000000000000000000000000000a","logsBloom":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f"}"#;
let receipt = Receipt {
transaction_hash: Some(0.into()),
@ -130,6 +130,7 @@ mod tests {
block_number: Some(0x4510c.into()),
transaction_hash: Some(0.into()),
transaction_index: Some(0.into()),
transaction_log_index: None,
log_index: Some(1.into()),
log_type: "mined".into(),
}],

View File

@ -1,103 +0,0 @@
// 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/>.
use std::default::Default;
use sha3::*;
use hash::H256;
use bytes::*;
use rlp::*;
use hashdb::*;
/// Type of operation for the backing database - either a new node or a node deletion.
#[derive(Debug)]
enum Operation {
New(H256, DBValue),
Delete(H256),
}
/// How many insertions and removals were done in an `apply` operation.
pub struct Score {
/// Number of insertions.
pub inserts: usize,
/// Number of removals.
pub removes: usize,
}
/// A journal of operations on the backing database.
#[derive(Debug)]
pub struct Journal (Vec<Operation>);
impl Default for Journal {
fn default() -> Self {
Journal::new()
}
}
impl Journal {
/// Create a new, empty, object.
pub fn new() -> Journal { Journal(vec![]) }
/// Given the RLP that encodes a node, append a reference to that node `out` and leave `journal`
/// such that the reference is valid, once applied.
pub fn new_node(&mut self, rlp: DBValue, out: &mut RlpStream) {
if rlp.len() >= 32 {
let rlp_sha3 = rlp.sha3();
trace!("new_node: reference node {:?} => {:?}", rlp_sha3, &*rlp);
out.append(&rlp_sha3);
self.0.push(Operation::New(rlp_sha3, rlp));
}
else {
trace!("new_node: inline node {:?}", &*rlp);
out.append_raw(&rlp, 1);
}
}
/// Given the RLP that encodes a now-unused node, leave `journal` in such a state that it is noted.
pub fn delete_node_sha3(&mut self, old_sha3: H256) {
trace!("delete_node: {:?}", old_sha3);
self.0.push(Operation::Delete(old_sha3));
}
/// Register an RLP-encoded node for deletion (given a slice), if it needs to be deleted.
pub fn delete_node(&mut self, old: &[u8]) {
let r = Rlp::new(old);
if r.is_data() && r.size() == 32 {
self.delete_node_sha3(r.as_val());
}
}
/// Apply this journal to the HashDB `db` and return the number of insertions and removals done.
pub fn apply(self, db: &mut HashDB) -> Score {
trace!("applying {:?} changes", self.0.len());
let mut ret = Score{inserts: 0, removes: 0};
for d in self.0 {
match d {
Operation::Delete(h) => {
trace!("TrieDBMut::apply --- {:?}", &h);
db.remove(&h);
ret.removes += 1;
},
Operation::New(h, d) => {
trace!("TrieDBMut::apply +++ {:?} -> {:?}", &h, d.pretty());
db.emplace(h, d);
ret.inserts += 1;
}
}
}
ret
}
}

View File

@ -22,8 +22,6 @@ use hashdb::{HashDB, DBValue};
/// Export the standardmap module.
pub mod standardmap;
/// Export the journal module.
pub mod journal;
/// Export the node module.
pub mod node;
/// Export the triedb module.

View File

@ -18,7 +18,6 @@ use elastic_array::ElasticArray36;
use nibbleslice::*;
use bytes::*;
use rlp::*;
use super::journal::*;
use hashdb::DBValue;
/// Partial node key type.
@ -123,44 +122,4 @@ impl Node {
}
}
}
/// Encode the node, adding it to `journal` if necessary and return the RLP valid for
/// insertion into a parent node.
pub fn encoded_and_added(&self, journal: &mut Journal) -> DBValue {
let mut stream = RlpStream::new();
match *self {
Node::Leaf(ref slice, ref value) => {
stream.begin_list(2);
stream.append(&&**slice);
stream.append(&&**value);
},
Node::Extension(ref slice, ref raw_rlp) => {
stream.begin_list(2);
stream.append(&&**slice);
stream.append_raw(&&**raw_rlp, 1);
},
Node::Branch(ref nodes, ref value) => {
stream.begin_list(17);
for i in 0..16 {
stream.append_raw(&*nodes[i], 1);
}
match *value {
Some(ref n) => { stream.append(&&**n); },
None => { stream.append_empty_data(); },
}
},
Node::Empty => {
stream.append_empty_data();
}
}
let node = DBValue::from_slice(stream.as_raw());
match node.len() {
0 ... 31 => node,
_ => {
let mut stream = RlpStream::new();
journal.new_node(node, &mut stream);
DBValue::from_slice(stream.as_raw())
}
}
}
}