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]] [[package]]
name = "parity-ui-precompiled" name = "parity-ui-precompiled"
version = "1.4.0" 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 = [ dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "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() .enumerate()
.flat_map(move |(index, (mut logs, tx_hash))| { .flat_map(move |(index, (mut logs, tx_hash))| {
let current_log_index = log_index; let current_log_index = log_index;
log_index -= logs.len(); let no_of_logs = logs.len();
log_index -= no_of_logs;
logs.reverse(); logs.reverse();
logs.into_iter() logs.into_iter()
@ -390,6 +391,7 @@ impl BlockProvider for BlockChain {
transaction_hash: tx_hash, transaction_hash: tx_hash,
// iterating in reverse order // iterating in reverse order
transaction_index: receipts_len - index - 1, transaction_index: receipts_len - index - 1,
transaction_log_index: no_of_logs - i - 1,
log_index: current_log_index - i - 1, log_index: current_log_index - i - 1,
}) })
}) })
@ -1936,6 +1938,7 @@ mod tests {
block_number: block1.header().number(), block_number: block1.header().number(),
transaction_hash: tx_hash1.clone(), transaction_hash: tx_hash1.clone(),
transaction_index: 0, transaction_index: 0,
transaction_log_index: 0,
log_index: 0, log_index: 0,
}, },
LocalizedLogEntry { LocalizedLogEntry {
@ -1944,6 +1947,7 @@ mod tests {
block_number: block1.header().number(), block_number: block1.header().number(),
transaction_hash: tx_hash1.clone(), transaction_hash: tx_hash1.clone(),
transaction_index: 0, transaction_index: 0,
transaction_log_index: 1,
log_index: 1, log_index: 1,
}, },
LocalizedLogEntry { LocalizedLogEntry {
@ -1952,6 +1956,7 @@ mod tests {
block_number: block1.header().number(), block_number: block1.header().number(),
transaction_hash: tx_hash2.clone(), transaction_hash: tx_hash2.clone(),
transaction_index: 1, transaction_index: 1,
transaction_log_index: 0,
log_index: 2, log_index: 2,
}, },
LocalizedLogEntry { LocalizedLogEntry {
@ -1960,6 +1965,7 @@ mod tests {
block_number: block2.header().number(), block_number: block2.header().number(),
transaction_hash: tx_hash3.clone(), transaction_hash: tx_hash3.clone(),
transaction_index: 0, transaction_index: 0,
transaction_log_index: 0,
log_index: 0, log_index: 0,
} }
]); ]);
@ -1970,6 +1976,7 @@ mod tests {
block_number: block2.header().number(), block_number: block2.header().number(),
transaction_hash: tx_hash3.clone(), transaction_hash: tx_hash3.clone(),
transaction_index: 0, transaction_index: 0,
transaction_log_index: 0,
log_index: 0, log_index: 0,
} }
]); ]);

View File

@ -59,7 +59,7 @@ use client::{
use client::Error as ClientError; use client::Error as ClientError;
use env_info::EnvInfo; use env_info::EnvInfo;
use executive::{Executive, Executed, TransactOptions, contract_address}; 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::{TraceDB, ImportRequest as TraceImportRequest, LocalizedTrace, Database as TraceDatabase};
use trace; use trace;
use trace::FlatTransactionTraces; use trace::FlatTransactionTraces;
@ -837,7 +837,6 @@ impl snapshot::DatabaseRestore for Client {
} }
} }
impl BlockChainClient for Client { impl BlockChainClient for Client {
fn call(&self, t: &SignedTransaction, block: BlockId, analytics: CallAnalytics) -> Result<Executed, CallError> { fn call(&self, t: &SignedTransaction, block: BlockId, analytics: CallAnalytics) -> Result<Executed, CallError> {
let header = self.block_header(block).ok_or(CallError::StatePruned)?; let header = self.block_header(block).ok_or(CallError::StatePruned)?;
@ -1134,53 +1133,23 @@ impl BlockChainClient for Client {
let chain = self.chain.read(); let chain = self.chain.read();
self.transaction_address(id) self.transaction_address(id)
.and_then(|address| chain.block_number(&address.block_hash).and_then(|block_number| { .and_then(|address| chain.block_number(&address.block_hash).and_then(|block_number| {
let t = chain.block_body(&address.block_hash) let transaction = chain.block_body(&address.block_hash)
.and_then(|body| { .and_then(|body| body.view().localized_transaction_at(&address.block_hash, block_number, address.index));
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))); let previous_receipts = (0..address.index + 1)
.map(|index| {
match (tx_and_sender, chain.transaction_receipt(&address)) { let mut address = address.clone();
(Some((tx, sender)), Some(receipt)) => { address.index = index;
let block_hash = tx.block_hash.clone(); chain.transaction_receipt(&address)
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,
}) })
}, .collect();
_ => None 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> { 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)] #[cfg(test)]
mod tests { mod tests {
@ -1570,4 +1582,91 @@ mod tests {
assert!(client.tree_route(&genesis, &new_hash).is_none()); 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.ready_transactions().len());
assert_eq!(2, client.miner().pending_transactions().len()); assert_eq!(2, client.miner().pending_transactions().len());
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -114,7 +114,11 @@ export default class Api {
} }
}) })
.catch((error) => { .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); reject(error);
}); });
}; };

View File

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

View File

@ -47,8 +47,6 @@ export function decodeMethodInput (methodAbi, paramdata) {
throw new Error('Input to decodeMethodInput should be a hex value'); throw new Error('Input to decodeMethodInput should be a hex value');
} else if (paramdata.substr(0, 2) === '0x') { } else if (paramdata.substr(0, 2) === '0x') {
return decodeMethodInput(methodAbi, paramdata.slice(2)); 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/); 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', () => { it('correctly decodes valid inputs', () => {
expect(decodeMethodInput({ expect(decodeMethodInput({
type: 'function', type: 'function',

View File

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

View File

@ -14,264 +14,255 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ContentAdd from 'material-ui/svg-icons/content/add'; import { FormattedMessage } from 'react-intl';
import ContentClear from 'material-ui/svg-icons/content/clear'; import { connect } from 'react-redux';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; import { bindActionCreators } from 'redux';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import { newError } from '~/redux/actions';
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui'; 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 = [ @observer
{ class AddContract extends Component {
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 {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} }
static propTypes = { static propTypes = {
contracts: PropTypes.object.isRequired, contracts: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func onClose: PropTypes.func
}; };
state = { store = new Store(this.context.api, this.props.contracts);
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);
}
render () { render () {
const { step } = this.state; const { step } = this.store;
return ( return (
<Modal <Modal
visible
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
steps={ STEPS }
current={ step } current={ step }
> steps={ [
{ this.renderStep(step) } <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> </Modal>
); );
} }
renderStep (step) { renderStep () {
const { step } = this.store;
switch (step) { switch (step) {
case 0: case 0:
return this.renderContractTypeSelector(); return this.renderContractTypeSelector();
default: default:
return this.renderFields(); return this.renderFields();
} }
} }
renderContractTypeSelector () { renderContractTypeSelector () {
const { abiTypeIndex } = this.state; const { abiTypeIndex, abiTypes } = this.store;
return ( return (
<RadioButtons <RadioButtons
name='contractType' name='contractType'
value={ abiTypeIndex } value={ abiTypeIndex }
values={ this.getAbiTypes() } values={ abiTypes }
onChange={ this.onChangeABIType } onChange={ this.onChangeABIType }
/> />
); );
} }
renderDialogActions () { renderDialogActions () {
const { addressError, nameError, step } = this.state; const { step } = this.store;
const hasError = !!(addressError || nameError);
const cancelBtn = ( const cancelBtn = (
<Button <Button
icon={ <ContentClear /> } icon={ <CancelIcon /> }
label='Cancel' key='cancel'
label={
<FormattedMessage
id='addContract.button.cancel'
defaultMessage='Cancel' />
}
onClick={ this.onClose } /> onClick={ this.onClose } />
); );
if (step === 0) { if (step === 0) {
const nextBtn = ( return [
cancelBtn,
<Button <Button
icon={ <NavigationArrowForward /> } icon={ <NextIcon /> }
label='Next' key='next'
label={
<FormattedMessage
id='addContract.button.next'
defaultMessage='Next' />
}
onClick={ this.onNext } /> onClick={ this.onNext } />
); ];
return [ cancelBtn, nextBtn ];
} }
const prevBtn = ( return [
cancelBtn,
<Button <Button
icon={ <NavigationArrowBack /> } icon={ <PrevIcon /> }
label='Back' key='prev'
onClick={ this.onPrev } /> label={
); <FormattedMessage
id='addContract.button.prev'
const addBtn = ( defaultMessage='Back' />
}
onClick={ this.onPrev } />,
<Button <Button
icon={ <ContentAdd /> } icon={ <AddIcon /> }
label='Add Contract' key='add'
disabled={ hasError } label={
<FormattedMessage
id='addContract.button.add'
defaultMessage='Add Contract' />
}
disabled={ this.store.hasError }
onClick={ this.onAdd } /> onClick={ this.onAdd } />
); ];
return [ cancelBtn, prevBtn, addBtn ];
} }
renderFields () { renderFields () {
const { abi, abiError, address, addressError, description, name, nameError, abiType } = this.state; const { abi, abiError, abiType, address, addressError, description, name, nameError } = this.store;
return ( return (
<Form> <Form>
<InputAddress <InputAddress
label='network address'
hint='the network address for the contract'
error={ addressError } error={ addressError }
value={ address } hint={
onSubmit={ this.onEditAddress } <FormattedMessage
id='addContract.address.hint'
defaultMessage='the network address for the contract' />
}
label={
<FormattedMessage
id='addContract.address.label'
defaultMessage='network address' />
}
onChange={ this.onChangeAddress } onChange={ this.onChangeAddress }
/> onSubmit={ this.onEditAddress }
value={ address } />
<Input <Input
label='contract name'
hint='a descriptive name for the contract'
error={ nameError } error={ nameError }
value={ name } hint={
onSubmit={ this.onEditName } /> <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 <Input
multiLine hint={
rows={ 1 } <FormattedMessage
label='(optional) contract description' id='addContract.description.hint'
hint='an expanded description for the entry' defaultMessage='an expanded description for the entry' />
value={ description } }
onSubmit={ this.onEditDescription } /> label={
<FormattedMessage
id='addContract.description.label'
defaultMessage='(optional) contract description' />
}
onSubmit={ this.onEditDescription }
value={ description } />
<Input <Input
label='contract abi'
hint='the abi for the contract'
error={ abiError } error={ abiError }
value={ abi } hint={
readOnly={ abiType.readOnly } <FormattedMessage
id='addContract.abi.hint'
defaultMessage='the abi for the contract' />
}
label={
<FormattedMessage
id='addContract.abi.label'
defaultMessage='contract abi' />
}
onSubmit={ this.onEditAbi } onSubmit={ this.onEditAbi }
/> readOnly={ abiType.readOnly }
value={ abi } />
</Form> </Form>
); );
} }
getAbiTypes () {
return ABI_TYPES.map((type, index) => ({
label: type.label,
description: type.description,
key: index,
...type
}));
}
onNext = () => { onNext = () => {
this.setState({ step: this.state.step + 1 }); this.store.nextStep();
} }
onPrev = () => { onPrev = () => {
this.setState({ step: this.state.step - 1 }); this.store.prevStep();
} }
onChangeABIType = (value, index) => { onChangeABIType = (value, index) => {
const abiType = value || ABI_TYPES[index]; this.store.setAbiTypeIndex(index);
this.setState({ abiTypeIndex: index, abiType });
this.onEditAbi(abiType.value);
} }
onEditAbi = (abiIn) => { onEditAbi = (abi) => {
const { api } = this.context; this.store.setAbi(abi);
const { abi, abiError, abiParsed } = validateAbi(abiIn, api);
this.setState({ abi, abiError, abiParsed });
} }
onChangeAddress = (event, value) => { onChangeAddress = (event, address) => {
this.onEditAddress(value); this.onEditAddress(address);
} }
onEditAddress = (_address) => { onEditAddress = (address) => {
const { contracts } = this.props; this.store.setAddress(address);
let { address, addressError } = validateAddress(_address);
if (!addressError) {
const contract = contracts[address];
if (contract) {
addressError = ERRORS.duplicateAddress;
}
}
this.setState({
address,
addressError
});
} }
onEditDescription = (description) => { onEditDescription = (description) => {
this.setState({ description }); this.store.setDescription(description);
} }
onEditName = (name) => { onEditName = (name) => {
this.setState(validateName(name)); this.store.setName(name);
} }
onAdd = () => { onAdd = () => {
const { api } = this.context; return this.store
const { abiParsed, address, name, description, abiType } = this.state; .addContract()
.then(() => {
Promise.all([ this.onClose();
api.parity.setAccountName(address, name),
api.parity.setAccountMeta(address, {
contract: true,
deleted: false,
timestamp: Date.now(),
abi: abiParsed,
type: abiType.type,
description
}) })
]).catch((error) => { .catch((error) => {
console.error('onAdd', error); this.props.newError(error);
}); });
this.props.onClose();
} }
onClose = () => { onClose = () => {
this.props.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'; import { FormattedMessage } from 'react-intl';
export default { export default {
noFile: noFile: (
<FormattedMessage <FormattedMessage
id='createAccount.error.noFile' id='createAccount.error.noFile'
defaultMessage='select a valid wallet file to import' />, defaultMessage='select a valid wallet file to import' />
),
noKey: noKey: (
<FormattedMessage <FormattedMessage
id='createAccount.error.noKey' 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 <FormattedMessage
id='createAccount.error.noMatchPassword' id='createAccount.error.noMatchPassword'
defaultMessage='the supplied passwords does not match' />, defaultMessage='the supplied passwords does not match' />
),
noName: noName: (
<FormattedMessage <FormattedMessage
id='createAccount.error.noName' 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 <FormattedMessage
id='createAccount.error.invalidKey' id='createAccount.error.invalidKey'
defaultMessage='the raw key needs to be hex, 64 characters in length and contain the prefix "0x"' /> 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/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { omitBy } from 'lodash';
import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui'; import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui';
@ -73,6 +74,9 @@ export default class WalletDetails extends Component {
renderMultisigDetails () { renderMultisigDetails () {
const { accounts, wallet, errors } = this.props; const { accounts, wallet, errors } = this.props;
// Wallets cannot create contracts
const _accounts = omitBy(accounts, (a) => a.wallet);
return ( return (
<Form> <Form>
<AddressSelect <AddressSelect
@ -81,7 +85,7 @@ export default class WalletDetails extends Component {
value={ wallet.account } value={ wallet.account }
error={ errors.account } error={ errors.account }
onChange={ this.onAccoutChange } onChange={ this.onAccoutChange }
accounts={ accounts } accounts={ _accounts }
/> />
<Input <Input

View File

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

View File

@ -17,20 +17,24 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; 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 { Button, Form, Input, InputChip, Modal } from '~/ui';
import { CancelIcon, SaveIcon } from '~/ui/Icons'; import { CancelIcon, SaveIcon } from '~/ui/Icons';
import Store from './store'; import Store from './store';
@observer @observer
export default class EditMeta extends Component { class EditMeta extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} }
static propTypes = { static propTypes = {
account: PropTypes.object.isRequired, account: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired onClose: PropTypes.func.isRequired
} }
@ -85,7 +89,7 @@ export default class EditMeta extends Component {
defaultMessage='(optional) tags' /> defaultMessage='(optional) tags' />
} }
onTokensChange={ this.store.setTags } onTokensChange={ this.store.setTags }
tokens={ tags } /> tokens={ tags.slice() } />
</Form> </Form>
</Modal> </Modal>
); );
@ -138,6 +142,20 @@ export default class EditMeta extends Component {
return this.store return this.store
.save() .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 EditMeta from './';
import { ACCOUNT } from './editMeta.test.js'; import { ACCOUNT, createApi, createRedux } from './editMeta.test.js';
let api;
let component; let component;
let instance;
let onClose; let onClose;
let reduxStore;
function render (props) { function render (props) {
api = createApi();
onClose = sinon.stub(); onClose = sinon.stub();
reduxStore = createRedux();
component = shallow( component = shallow(
<EditMeta <EditMeta
{ ...props } { ...props }
account={ ACCOUNT } account={ ACCOUNT }
onClose={ onClose } />, onClose={ onClose } />,
{ { context: { store: reduxStore } }
context: { ).find('EditMeta').shallow({ context: { api } });
api: { instance = component.instance();
parity: {
setAccountName: sinon.stub().resolves(),
setAccountMeta: sinon.stub().resolves()
}
}
}
}
);
return component; return component;
} }
@ -61,15 +58,29 @@ describe('modals/EditMeta', () => {
}); });
describe('onSave', () => { describe('onSave', () => {
it('calls store.save() & props.onClose', () => { it('calls store.save', () => {
const instance = component.instance();
sinon.spy(instance.store, 'save'); sinon.spy(instance.store, 'save');
instance.onSave().then(() => { return instance.onSave().then(() => {
expect(instance.store.save).to.have.been.called; 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; 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 // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
const ACCOUNT = { const ACCOUNT = {
address: '0x123456789a123456789a123456789a123456789a', address: '0x123456789a123456789a123456789a123456789a',
meta: { meta: {
@ -39,7 +41,28 @@ const ADDRESS = {
name: 'Random 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 { export {
ACCOUNT, ACCOUNT,
ADDRESS ADDRESS,
createApi,
createRedux
}; };

View File

@ -14,34 +14,35 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // 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'; import { validateName } from '~/util/validation';
export default class Store { export default class Store {
@observable address = null; @observable address = null;
@observable isAccount = false; @observable isAccount = false;
@observable description = null; @observable description = null;
@observable meta = {}; @observable meta = null;
@observable name = null; @observable name = null;
@observable nameError = null; @observable nameError = null;
@observable passwordHint = null; @observable passwordHint = null;
@observable tags = []; @observable tags = null;
constructor (api, account) { constructor (api, account) {
const { address, name, meta, uuid } = account; const { address, name, meta, uuid } = account;
this._api = api; this._api = api;
this.isAccount = !!uuid; transaction(() => {
this.address = address; this.isAccount = !!uuid;
this.meta = Object.assign({}, meta || {}); this.address = address;
this.name = name || ''; this.meta = meta || {};
this.name = name || '';
this.description = this.meta.description || ''; this.description = this.meta.description || '';
this.passwordHint = this.meta.passwordHint || ''; this.passwordHint = this.meta.passwordHint || '';
this.tags = [].concat((meta || {}).tags || []); this.tags = this.meta.tags && this.meta.tags.peek() || [];
});
} }
@computed get hasError () { @computed get hasError () {
@ -70,7 +71,7 @@ export default class Store {
} }
@action setTags = (tags) => { @action setTags = (tags) => {
this.tags = [].concat(tags); this.tags = tags.slice();
} }
save () { save () {
@ -86,12 +87,11 @@ export default class Store {
return Promise return Promise
.all([ .all([
this._api.parity.setAccountName(this.address, this.name), 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) => { .catch((error) => {
console.error('onSave', error); console.error('onSave', error);
throw error;
newError(error);
}); });
} }
} }

View File

@ -14,22 +14,14 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { toJS } from 'mobx';
import sinon from 'sinon';
import Store from './store'; import Store from './store';
import { ACCOUNT, ADDRESS } from './editMeta.test.js'; import { ACCOUNT, ADDRESS, createApi } from './editMeta.test.js';
let api; let api;
let store; let store;
function createStore (account) { function createStore (account) {
api = { api = createApi();
parity: {
setAccountName: sinon.stub().resolves(),
setAccountMeta: sinon.stub().resolves()
}
};
store = new Store(api, account); store = new Store(api, account);
@ -56,12 +48,12 @@ describe('modals/EditMeta/Store', () => {
}); });
it('extracts the tags', () => { 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', () => { describe('meta', () => {
it('extracts the full 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', () => { it('extracts the description', () => {

View File

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

View File

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

View File

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

View File

@ -14,26 +14,36 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // 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 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 { connect } from 'react-redux';
import { bindActionCreators } from '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 { 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'; import styles from './passwordManager.css';
const TEST_ACTION = 'TEST_ACTION'; const MSG_SUCCESS_STYLE = {
const CHANGE_ACTION = 'CHANGE_ACTION'; 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 { class PasswordManager extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
@ -41,27 +51,22 @@ class PasswordManager extends Component {
static propTypes = { static propTypes = {
account: PropTypes.object.isRequired, account: PropTypes.object.isRequired,
showSnackbar: PropTypes.func.isRequired, openSnackbar: PropTypes.func.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func onClose: PropTypes.func
} }
state = { store = new Store(this.context.api, this.props.account);
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 || ''
}
render () { render () {
return ( return (
<Modal <Modal
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
title='Password Manager' title={
<FormattedMessage
id='passwordChange.title'
defaultMessage='Password Manager' />
}
visible> visible>
{ this.renderAccount() } { this.renderAccount() }
{ this.renderPage() } { this.renderPage() }
@ -71,150 +76,168 @@ class PasswordManager extends Component {
} }
renderMessage () { renderMessage () {
const { message, showMessage } = this.state; const { infoMessage } = this.store;
const style = message.success if (!infoMessage) {
? { return null;
backgroundColor: 'rgba(174, 213, 129, 0.75)'
}
: {
backgroundColor: 'rgba(229, 115, 115, 0.75)'
};
const classes = [ styles.message ];
if (!showMessage) {
classes.push(styles.hideMessage);
} }
return ( return (
<Paper <Paper
zDepth={ 1 } className={ `${styles.message}` }
style={ style } style={
className={ classes.join(' ') }> infoMessage.success
{ message.value } ? MSG_SUCCESS_STYLE
: MSG_FAILURE_STYLE
}
zDepth={ 1 }>
{ infoMessage.value }
</Paper> </Paper>
); );
} }
renderAccount () { renderAccount () {
const { account } = this.props; const { address, passwordHint } = this.store;
const { address, meta } = account;
const passwordHint = meta && meta.passwordHint
? (
<span className={ styles.passwordHint }>
<span className={ styles.hintLabel }>Hint </span>
{ meta.passwordHint }
</span>
)
: null;
return ( return (
<div className={ styles.accountContainer }> <div className={ styles.accountContainer }>
<IdentityIcon <IdentityIcon address={ address } />
address={ address }
/>
<div className={ styles.accountInfos }> <div className={ styles.accountInfos }>
<IdentityName <IdentityName
className={ styles.accountName }
address={ address } address={ address }
unknown className={ styles.accountName }
/> unknown />
<span className={ styles.accountAddress }> <span className={ styles.accountAddress }>
{ address } { address }
</span> </span>
{ passwordHint } <span className={ styles.passwordHint }>
<span className={ styles.hintLabel }>Hint </span>
{ passwordHint || '-' }
</span>
</div> </div>
</div> </div>
); );
} }
renderPage () { renderPage () {
const { account } = this.props; const { busy, isRepeatValid, passwordHint } = this.store;
const { waiting, repeatValid } = this.state;
const disabled = !!waiting;
const repeatError = repeatValid
? null
: 'the two passwords differ';
const { meta } = account;
const passwordHint = meta && meta.passwordHint || '';
return ( return (
<Tabs <Tabs
inkBarStyle={ { inkBarStyle={ TABS_INKBAR_STYLE }
backgroundColor: 'rgba(255, 255, 255, 0.55)' tabItemContainerStyle={ TABS_ITEM_STYLE }>
} }
tabItemContainerStyle={ {
backgroundColor: 'rgba(255, 255, 255, 0.05)'
} }
>
<Tab <Tab
onActive={ this.handleTestActive } label={
label='Test Password' <FormattedMessage
> id='passwordChange.tabTest.label'
<Form defaultMessage='Test Password' />
className={ styles.form } }
> onActive={ this.onActivateTestTab }>
<Form className={ styles.form }>
<div> <div>
<Input <Input
label='password' disabled={ busy }
hint='your current password for this account' hint={
type='password' <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 } submitOnBlur={ false }
disabled={ disabled } type='password' />
onSubmit={ this.handleTestPassword }
onChange={ this.onEditCurrent } />
</div> </div>
</Form> </Form>
</Tab> </Tab>
<Tab <Tab
onActive={ this.handleChangeActive } label={
label='Change Password' <FormattedMessage
> id='passwordChange.tabChange.label'
<Form defaultMessage='Change Password' />
className={ styles.form } }
> onActive={ this.onActivateChangeTab }>
<Form className={ styles.form }>
<div> <div>
<Input <Input
label='current password' disabled={ busy }
hint='your current password for this account' hint={
type='password' <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 } submitOnBlur={ false }
disabled={ disabled } type='password' />
onSubmit={ this.handleChangePassword }
onChange={ this.onEditCurrent } />
<Input <Input
label='(optional) new password hint' disabled={ busy }
hint='hint for the new password' 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 } submitOnBlur={ false }
value={ passwordHint } value={ passwordHint } />
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditHint } />
<div className={ styles.passwords }> <div className={ styles.passwords }>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
label='new password' disabled={ busy }
hint='the new password for this account' hint={
type='password' <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 } submitOnBlur={ false }
disabled={ disabled } type='password' />
onSubmit={ this.handleChangePassword }
onChange={ this.onEditNew } />
</div> </div>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
label='repeat new password' disabled={ busy }
hint='repeat the new password for this account' error={
type='password' 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 } submitOnBlur={ false }
error={ repeatError } type='password' />
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditRepeatNew } />
</div> </div>
</div> </div>
</div> </div>
@ -225,172 +248,126 @@ class PasswordManager extends Component {
} }
renderDialogActions () { renderDialogActions () {
const { actionTab, busy, isRepeatValid } = this.store;
const { onClose } = this.props; const { onClose } = this.props;
const { action, waiting, repeatValid } = this.state;
const cancelBtn = ( const cancelBtn = (
<Button <Button
icon={ <ContentClear /> } icon={ <CancelIcon /> }
label='Cancel' key='cancel'
label={
<FormattedMessage
id='passwordChange.button.cancel'
defaultMessage='Cancel' />
}
onClick={ onClose } /> onClick={ onClose } />
); );
if (waiting) { if (busy) {
const waitingBtn = ( return [
cancelBtn,
<Button <Button
disabled disabled
label='Wait...' /> key='wait'
); label={
<FormattedMessage
return [ cancelBtn, waitingBtn ]; id='passwordChange.button.wait'
defaultMessage='Wait...' />
} />
];
} }
if (action === TEST_ACTION) { if (actionTab === TEST_ACTION) {
const testBtn = ( return [
cancelBtn,
<Button <Button
icon={ <CheckIcon /> } icon={ <CheckIcon /> }
label='Test' key='test'
onClick={ this.handleTestPassword } /> label={
); <FormattedMessage
id='passwordChange.button.test'
return [ cancelBtn, testBtn ]; defaultMessage='Test' />
}
onClick={ this.testPassword } />
];
} }
const changeBtn = ( return [
cancelBtn,
<Button <Button
disabled={ !repeatValid } disabled={ !isRepeatValid }
icon={ <SendIcon /> } icon={ <SendIcon /> }
label='Change' key='change'
onClick={ this.handleChangePassword } /> label={
); <FormattedMessage
id='passwordChange.button.change'
return [ cancelBtn, changeBtn ]; defaultMessage='Change' />
}
onClick={ this.changePassword } />
];
} }
onEditCurrent = (event, value) => { onActivateChangeTab = () => {
this.setState({ this.store.setActionTab(CHANGE_ACTION);
currentPass: value,
showMessage: false
});
} }
onEditNew = (event, value) => { onActivateTestTab = () => {
const repeatValid = value === this.state.repeatNewPass; this.store.setActionTab(TEST_ACTION);
this.setState({
newPass: value,
showMessage: false,
repeatValid
});
} }
onEditRepeatNew = (event, value) => { onEditCurrentPassword = (event, password) => {
const repeatValid = value === this.state.newPass; this.store.setPassword(password);
this.setState({
repeatNewPass: value,
showMessage: false,
repeatValid
});
} }
onEditHint = (event, value) => { onEditNewPassword = (event, password) => {
this.setState({ this.store.setNewPassword(password);
passwordHint: value,
showMessage: false
});
} }
handleTestActive = () => { onEditNewPasswordHint = (event, passwordHint) => {
this.setState({ this.store.setNewPasswordHint(passwordHint);
action: TEST_ACTION,
showMessage: false
});
} }
handleChangeActive = () => { onEditNewPasswordRepeat = (event, password) => {
this.setState({ this.store.setNewPasswordRepeat(password);
action: CHANGE_ACTION,
showMessage: false
});
} }
handleTestPassword = () => { onEditTestPassword = (event, password) => {
const { account } = this.props; this.store.setValidatePassword(password);
const { currentPass } = this.state; }
this.setState({ waiting: true, showMessage: false }); changePassword = () => {
return this.store
this.context .changePassword()
.api.parity .then((result) => {
.testPassword(account.address, currentPass) if (result) {
.then(correct => { this.props.openSnackbar(
const message = correct <div>
? { value: 'This password is correct', success: true } <FormattedMessage
: { value: 'This password is not correct', success: false }; id='passwordChange.success'
defaultMessage='Your password has been successfully changed' />
this.setState({ waiting: false, message, showMessage: true }); </div>
);
this.props.onClose();
}
}) })
.catch(e => { .catch((error) => {
console.error('passwordManager::handleTestPassword', e); this.props.newError(error);
this.setState({ waiting: false });
}); });
} }
handleChangePassword = () => { testPassword = () => {
const { account, showSnackbar, onClose } = this.props; return this.store
const { currentPass, newPass, repeatNewPass, passwordHint } = this.state; .testPassword()
.catch((error) => {
if (repeatNewPass !== newPass) { this.props.newError(error);
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 });
}); });
} }
} }
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return bindActionCreators({ return bindActionCreators({
showSnackbar openSnackbar,
newError
}, dispatch); }, 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 senderBalance = this.balance.tokens.find((b) => tag === b.token.tag);
const format = new BigNumber(senderBalance.token.format || 1); const format = new BigNumber(senderBalance.token.format || 1);
const available = isWallet const available = new BigNumber(senderBalance.value).div(format);
? this.api.util.fromWei(new BigNumber(senderBalance.value))
: (new BigNumber(senderBalance.value)).div(format);
let { value, valueError } = this; let { value, valueError } = this;
let totalEth = gasTotal; let totalEth = gasTotal;
@ -428,7 +426,6 @@ export default class TransferStore {
send () { send () {
const { options, values } = this._getTransferParams(); const { options, values } = this._getTransferParams();
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null; options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
return this._getTransferMethod().postTransaction(options, values); return this._getTransferMethod().postTransaction(options, values);
@ -440,16 +437,7 @@ export default class TransferStore {
} }
estimateGas () { estimateGas () {
if (this.isEth || !this.isWallet) { return this._estimateGas();
return this._estimateGas();
}
return Promise
.all([
this._estimateGas(true),
this._estimateGas()
])
.then((results) => results[0].plus(results[1]));
} }
_getTransferMethod (gas = false, forceToken = false) { _getTransferMethod (gas = false, forceToken = false) {

View File

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

View File

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

View File

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

View File

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

View File

@ -173,17 +173,18 @@ export function fetchTokens (_tokenIds) {
export function fetchBalances (_addresses) { export function fetchBalances (_addresses) {
return (dispatch, getState) => { return (dispatch, getState) => {
const { api, personal } = 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) { if (addresses.length === 0) {
return Promise.resolve(); return Promise.resolve();
} }
// With only a single account, more info will be displayed.
const fullFetch = addresses.length === 1; const fullFetch = addresses.length === 1;
const addressesToFetch = uniq(addresses.concat(Object.keys(accounts))); const addressesToFetch = uniq(addresses);
return Promise return Promise
.all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch))) .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 // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // 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 { fetchBalances } from './balancesActions';
import { attachWallets } from './walletActions'; import { attachWallets } from './walletActions';
import Contract from '~/api/contract';
import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore'; import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore';
import WalletsUtils from '~/util/wallets';
import { wallet as WalletAbi } from '~/contracts/abi';
export function personalAccountsInfo (accountsInfo) { export function personalAccountsInfo (accountsInfo) {
const addresses = [];
const accounts = {}; const accounts = {};
const contacts = {}; const contacts = {};
const contracts = {}; const contracts = {};
@ -32,6 +36,7 @@ export function personalAccountsInfo (accountsInfo) {
.filter((account) => account.uuid || !account.meta.deleted) .filter((account) => account.uuid || !account.meta.deleted)
.forEach((account) => { .forEach((account) => {
if (account.uuid) { if (account.uuid) {
addresses.push(account.address);
accounts[account.address] = account; accounts[account.address] = account;
} else if (account.meta.wallet) { } else if (account.meta.wallet) {
account.wallet = true; account.wallet = true;
@ -46,14 +51,52 @@ export function personalAccountsInfo (accountsInfo) {
// Load user contracts for Method Decoding // Load user contracts for Method Decoding
MethodDecodingStore.loadContracts(contracts); MethodDecodingStore.loadContracts(contracts);
return (dispatch) => { return (dispatch, getState) => {
const data = { const { api } = getState();
accountsInfo,
accounts, contacts, contracts, wallets
};
dispatch(_personalAccountsInfo(data)); const _fetchOwners = Object
dispatch(attachWallets(wallets)); .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, hasContacts: false,
contracts: {}, contracts: {},
hasContracts: false, hasContracts: false,
wallet: {},
hasWallets: false,
visibleAccounts: [] visibleAccounts: []
}; };
export default handleActions({ export default handleActions({
personalAccountsInfo (state, action) { 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, { return Object.assign({}, state, {
accountsInfo, accountsInfo,
@ -41,9 +40,7 @@ export default handleActions({
contacts, contacts,
hasContacts: Object.keys(contacts).length !== 0, hasContacts: Object.keys(contacts).length !== 0,
contracts, contracts,
hasContracts: Object.keys(contracts).length !== 0, hasContracts: Object.keys(contracts).length !== 0
wallets,
hasWallets: Object.keys(wallets).length !== 0
}); });
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,14 +19,17 @@ import ReactDOM from 'react-dom';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import keycode, { codes } from 'keycode'; import keycode, { codes } from 'keycode';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { observer } from 'mobx-react';
import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline'; import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline';
import AccountCard from '~/ui/AccountCard'; import AccountCard from '~/ui/AccountCard';
import InputAddress from '~/ui/Form/InputAddress'; import InputAddress from '~/ui/Form/InputAddress';
import Portal from '~/ui/Portal'; import Portal from '~/ui/Portal';
import { nodeOrStringProptype } from '~/util/proptypes';
import { validateAddress } from '~/util/validation'; import { validateAddress } from '~/util/validation';
import AddressSelectStore from './addressSelectStore';
import styles from './addressSelect.css'; import styles from './addressSelect.css';
const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' }; const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' };
@ -34,8 +37,11 @@ const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' };
// Current Form ID // Current Form ID
let currentId = 1; let currentId = 1;
@observer
class AddressSelect extends Component { class AddressSelect extends Component {
static contextTypes = { static contextTypes = {
intl: React.PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
muiTheme: PropTypes.object.isRequired muiTheme: PropTypes.object.isRequired
}; };
@ -50,29 +56,31 @@ class AddressSelect extends Component {
contacts: PropTypes.object, contacts: PropTypes.object,
contracts: PropTypes.object, contracts: PropTypes.object,
tokens: PropTypes.object, tokens: PropTypes.object,
wallets: PropTypes.object,
// Optional props // Optional props
allowCopy: PropTypes.bool,
allowInput: PropTypes.bool, allowInput: PropTypes.bool,
disabled: PropTypes.bool, disabled: PropTypes.bool,
error: PropTypes.string, error: nodeOrStringProptype(),
hint: PropTypes.string, hint: nodeOrStringProptype(),
label: PropTypes.string, label: nodeOrStringProptype(),
value: PropTypes.string readOnly: PropTypes.bool,
value: nodeOrStringProptype()
}; };
static defaultProps = { static defaultProps = {
value: '' value: ''
}; };
store = new AddressSelectStore(this.context.api);
state = { state = {
expanded: false, expanded: false,
focused: false, focused: false,
focusedCat: null, focusedCat: null,
focusedItem: null, focusedItem: null,
inputFocused: false, inputFocused: false,
inputValue: '', inputValue: ''
values: []
}; };
componentWillMount () { componentWillMount () {
@ -80,7 +88,7 @@ class AddressSelect extends Component {
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (this.values && this.values.length > 0) { if (this.store.values && this.store.values.length > 0) {
return; return;
} }
@ -88,36 +96,7 @@ class AddressSelect extends Component {
} }
setValues (props = this.props) { setValues (props = this.props) {
const { accounts = {}, contracts = {}, contacts = {}, wallets = {} } = props; this.store.setValues(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();
} }
render () { render () {
@ -144,12 +123,12 @@ class AddressSelect extends Component {
renderInput () { renderInput () {
const { focused } = this.state; 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 = ( const input = (
<InputAddress <InputAddress
accountsInfo={ accountsInfo } accountsInfo={ accountsInfo }
allowCopy={ false } allowCopy={ allowCopy }
disabled={ disabled } disabled={ disabled }
error={ error } error={ error }
hint={ hint } hint={ hint }
@ -162,7 +141,7 @@ class AddressSelect extends Component {
/> />
); );
if (disabled) { if (disabled || readOnly) {
return input; return input;
} }
@ -175,14 +154,20 @@ class AddressSelect extends Component {
renderContent () { renderContent () {
const { muiTheme } = this.context; const { muiTheme } = this.context;
const { hint, disabled, label } = this.props; const { hint, disabled, label, readOnly } = this.props;
const { expanded, inputFocused } = this.state; const { expanded, inputFocused } = this.state;
if (disabled) { if (disabled || readOnly) {
return null; return null;
} }
const id = `addressSelect_${++currentId}`; const id = `addressSelect_${++currentId}`;
const ilHint = typeof hint === 'string' || !(hint && hint.props)
? (hint || '')
: this.context.intl.formatMessage(
hint.props,
hint.props.values || {}
);
return ( return (
<Portal <Portal
@ -197,7 +182,7 @@ class AddressSelect extends Component {
<input <input
id={ id } id={ id }
className={ styles.input } className={ styles.input }
placeholder={ hint } placeholder={ ilHint }
onBlur={ this.handleInputBlur } onBlur={ this.handleInputBlur }
onFocus={ this.handleInputFocus } onFocus={ this.handleInputFocus }
@ -216,6 +201,7 @@ class AddressSelect extends Component {
</div> </div>
{ this.renderCurrentInput() } { this.renderCurrentInput() }
{ this.renderRegistryValues() }
{ this.renderAccounts() } { this.renderAccounts() }
</Portal> </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 () { renderAccounts () {
const { values } = this.state; const { values } = this.store;
if (values.length === 0) { if (values.length === 0) {
return ( return (
@ -257,8 +263,8 @@ class AddressSelect extends Component {
); );
} }
const categories = values.map((category) => { const categories = values.map((category, index) => {
return this.renderCategory(category.label, category.values); return this.renderCategory(category, index);
}); });
return ( return (
@ -268,7 +274,8 @@ class AddressSelect extends Component {
); );
} }
renderCategory (name, values = []) { renderCategory (category, index) {
const { label, key, values = [] } = category;
let content; let content;
if (values.length === 0) { if (values.length === 0) {
@ -292,8 +299,8 @@ class AddressSelect extends Component {
} }
return ( return (
<div className={ styles.category } key={ name }> <div className={ styles.category } key={ `${key}_${index}` }>
<div className={ styles.title }>{ name }</div> <div className={ styles.title }>{ label }</div>
{ content } { content }
</div> </div>
); );
@ -306,7 +313,7 @@ class AddressSelect extends Component {
const balance = balances[address]; const balance = balances[address];
const account = { const account = {
...accountsInfo[address], ...accountsInfo[address],
address, index ..._account
}; };
return ( return (
@ -325,9 +332,10 @@ class AddressSelect extends Component {
this.inputRef = refId; this.inputRef = refId;
} }
handleCustomInput = () => { validateCustomInput = () => {
const { allowInput } = this.props; 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 input is HEX and allowInput === true, send it
if (allowInput && inputValue && /^(0x)?([0-9a-f])+$/i.test(inputValue)) { 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 only one value, select it
if (values.length === 1 && values[0].values.length === 1) { if (values.reduce((cur, cat) => cur + cat.values.length, 0) === 1) {
const value = values[0].values[0]; const value = values.find((cat) => cat.values.length > 0).values[0];
return this.handleClick(value.address); return this.handleClick(value.address);
} }
} }
@ -361,7 +369,7 @@ class AddressSelect extends Component {
case 'enter': case 'enter':
const index = this.state.focusedItem; const index = this.state.focusedItem;
if (!index) { if (!index) {
return this.handleCustomInput(); return this.validateCustomInput();
} }
return this.handleDOMAction(`account_${index}`, 'click'); return this.handleDOMAction(`account_${index}`, 'click');
@ -408,10 +416,11 @@ class AddressSelect extends Component {
} }
handleNavigation = (direction, event) => { 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 // Don't do anything if no values
if (values.length === 0) { if (values.reduce((cur, cat) => cur + cat.values.length, 0) === 0) {
return event; return event;
} }
@ -423,7 +432,12 @@ class AddressSelect extends Component {
event.preventDefault(); 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; const nextFocus = nextValues ? nextValues.values[0] : null;
return this.focusItem(nextFocus && nextFocus.index || 1); return this.focusItem(nextFocus && nextFocus.index || 1);
} }
@ -457,12 +471,21 @@ class AddressSelect extends Component {
// If right: next category // If right: next category
if (direction === 'right') { 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 right: previous category
if (direction === 'left') { 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 // If left or right: try to keep the horizontal index
@ -486,6 +509,10 @@ class AddressSelect extends Component {
} }
handleMainBlur = () => { handleMainBlur = () => {
if (this.props.readOnly) {
return;
}
if (window.document.hasFocus() && !this.state.expanded) { if (window.document.hasFocus() && !this.state.expanded) {
this.closing = false; this.closing = false;
this.setState({ focused: false }); this.setState({ focused: false });
@ -493,7 +520,7 @@ class AddressSelect extends Component {
} }
handleMainFocus = () => { handleMainFocus = () => {
if (this.state.focused) { if (this.state.focused || this.props.readOnly) {
return; return;
} }
@ -508,6 +535,12 @@ class AddressSelect extends Component {
} }
handleFocus = () => { handleFocus = () => {
const { disabled, readOnly } = this.props;
if (disabled || readOnly) {
return;
}
this.setState({ expanded: true, focusedItem: null, focusedCat: null }, () => { this.setState({ expanded: true, focusedItem: null, focusedCat: null }, () => {
window.setTimeout(() => { window.setTimeout(() => {
this.handleDOMAction(this.inputRef, 'focus'); this.handleDOMAction(this.inputRef, 'focus');
@ -525,43 +558,6 @@ class AddressSelect extends Component {
this.setState({ expanded: false }); 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 = () => { handleInputBlur = () => {
this.setState({ inputFocused: false }); this.setState({ inputFocused: false });
} }
@ -572,25 +568,10 @@ class AddressSelect extends Component {
handleChange = (event = { target: {} }) => { handleChange = (event = { target: {} }) => {
const { value = '' } = event.target; const { value = '' } = event.target;
let index = 0;
const values = this.values this.store.handleChange(value);
.map((category) => {
const filteredValues = this
.filterValues(category.values, value)
.map((value) => {
index++;
return { ...value, index: parseInt(index) };
});
return {
label: category.label,
values: filteredValues
};
});
this.setState({ this.setState({
values,
focusedItem: null, focusedItem: null,
inputValue: value 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.string,
PropTypes.bool PropTypes.bool
]), ]),
autoFocus: PropTypes.bool,
children: PropTypes.node, children: PropTypes.node,
className: PropTypes.string, className: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
@ -112,7 +113,7 @@ export default class Input extends Component {
render () { render () {
const { value } = this.state; 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 { hint, onClick, onFocus, multiLine, rows, type, min, max, style, tabIndex } = this.props;
const readOnly = this.props.readOnly || disabled; const readOnly = this.props.readOnly || disabled;
@ -138,6 +139,7 @@ export default class Input extends Component {
{ this.renderCopyButton() } { this.renderCopyButton() }
<TextField <TextField
autoComplete='off' autoComplete='off'
autoFocus={ autoFocus }
className={ className } className={ className }
errorText={ error } errorText={ error }
floatingLabelFixed floatingLabelFixed
@ -183,7 +185,7 @@ export default class Input extends Component {
const text = typeof allowCopy === 'string' const text = typeof allowCopy === 'string'
? allowCopy ? allowCopy
: value; : value.toString();
const style = hideUnderline const style = hideUnderline
? {} ? {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,32 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* 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 { .parityBackground {
position: absolute; position: absolute;
top: 0; top: 0;
@ -28,10 +54,10 @@
.overlay { .overlay {
display: flex; display: flex;
position: fixed; position: fixed;
top: 0; top: $top;
left: 0; left: $left;
width: 100vw; width: calc(100vw - $left - $right);
height: 100vh; height: calc(100vh - $top - $bottom);
transform-origin: 100% 0; transform-origin: 100% 0;
transition-property: opacity, z-index; transition-property: opacity, z-index;
@ -48,7 +74,7 @@
&.expanded { &.expanded {
opacity: 1; opacity: 1;
z-index: 9999; z-index: 3500;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -14,10 +14,93 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import WalletsUtils from '~/util/wallets';
const isValidReceipt = (receipt) => { const isValidReceipt = (receipt) => {
return receipt && receipt.blockNumber && receipt.blockNumber.gt(0); 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) { export function checkIfTxFailed (api, tx, gasSent) {
return api.pollMethod('eth_getTransactionReceipt', tx) return api.pollMethod('eth_getTransactionReceipt', tx)
.then((receipt) => { .then((receipt) => {

View File

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

View File

@ -14,13 +14,92 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // 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 { bytesToHex, toHex } from '~/api/util/format';
import { validateAddress } from '~/util/validation'; import { validateAddress } from '~/util/validation';
import WalletAbi from '~/contracts/abi/wallet.json';
const _cachedWalletLookup = {};
export default class WalletsUtils { 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) { static fetchRequire (walletContract) {
return walletContract.instance.m_required.call(); return walletContract.instance.m_required.call();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,12 +14,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Card, CardTitle, CardText } from 'material-ui/Card'; import { Card, CardTitle, CardText } from 'material-ui/Card';
import InputQuery from './inputQuery'; import InputQuery from './inputQuery';
import { Container, Input, InputAddress } from '~/ui'; import { Container, TypedInput } from '~/ui';
import styles from './queries.css'; import styles from './queries.css';
@ -29,6 +28,7 @@ export default class Queries extends Component {
} }
static propTypes = { static propTypes = {
accountsInfo: PropTypes.object.isRequired,
contract: PropTypes.object, contract: PropTypes.object,
values: PropTypes.object values: PropTypes.object
} }
@ -74,11 +74,12 @@ export default class Queries extends Component {
renderInputQuery (fn) { renderInputQuery (fn) {
const { abi, name, signature } = fn; const { abi, name, signature } = fn;
const { contract } = this.props; const { accountsInfo, contract } = this.props;
return ( return (
<div className={ styles.container } key={ fn.signature }> <div className={ styles.container } key={ fn.signature }>
<InputQuery <InputQuery
accountsInfo={ accountsInfo }
className={ styles.method } className={ styles.method }
inputs={ abi.inputs } inputs={ abi.inputs }
outputs={ abi.outputs } outputs={ abi.outputs }
@ -116,34 +117,23 @@ export default class Queries extends Component {
} }
const { api } = this.context; const { api } = this.context;
let valueToDisplay = null; const { accountsInfo } = this.props;
if (api.util.isInstanceOf(value, BigNumber)) { let valueToDisplay = value;
valueToDisplay = value.toFormat(0);
} else if (api.util.isArray(value)) { if (api.util.isArray(value)) {
valueToDisplay = api.util.bytesToHex(value); valueToDisplay = api.util.bytesToHex(value);
} else if (typeof value === 'boolean') { } else if (typeof value === 'boolean') {
valueToDisplay = value ? 'true' : 'false'; valueToDisplay = value ? 'true' : 'false';
} else {
valueToDisplay = value.toString();
} }
if (type === 'address') {
return (
<InputAddress
className={ styles.queryValue }
value={ valueToDisplay }
disabled
/>
);
}
return ( return (
<Input <TypedInput
className={ styles.queryValue } accounts={ accountsInfo }
value={ valueToDisplay }
readOnly
allowCopy allowCopy
isEth={ false }
param={ type }
readOnly
value={ valueToDisplay }
/> />
); );
} }

View File

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

View File

@ -20,6 +20,7 @@ import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { omitBy } from 'lodash';
import { AddDapps, DappPermissions } from '~/modals'; import { AddDapps, DappPermissions } from '~/modals';
import PermissionStore from '~/modals/DappPermissions/store'; import PermissionStore from '~/modals/DappPermissions/store';
@ -150,8 +151,15 @@ class Dapps extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts } = state.personal; 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 { return {
accounts accounts: _accounts
}; };
} }

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
@ -26,11 +27,16 @@ import styles from './transactionPendingFormConfirm.css';
class TransactionPendingFormConfirm extends Component { class TransactionPendingFormConfirm extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, account: PropTypes.object.isRequired,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
isSending: PropTypes.bool.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 id = Math.random(); // for tooltip
@ -40,10 +46,39 @@ class TransactionPendingFormConfirm extends Component {
walletError: null 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 () { render () {
const { accounts, address, isSending } = this.props; const { account, address, isSending } = this.props;
const { password, wallet, walletError } = this.state; const { password, wallet, walletError } = this.state;
const account = accounts[address] || {};
const isExternal = !account.uuid; const isExternal = !account.uuid;
const passwordHint = account.meta && account.meta.passwordHint const passwordHint = account.meta && account.meta.passwordHint
@ -72,8 +107,10 @@ class TransactionPendingFormConfirm extends Component {
} }
onChange={ this.onModifyPassword } onChange={ this.onModifyPassword }
onKeyDown={ this.onKeyDown } onKeyDown={ this.onKeyDown }
ref='input'
type='password' type='password'
value={ password } /> value={ password }
/>
<div className={ styles.passwordHint }> <div className={ styles.passwordHint }>
{ passwordHint } { passwordHint }
</div> </div>
@ -178,11 +215,14 @@ class TransactionPendingFormConfirm extends Component {
} }
} }
function mapStateToProps (state) { function mapStateToProps (initState, initProps) {
const { accounts } = state.personal; const { accounts } = initState.personal;
const { address } = initProps;
return { const account = accounts[address] || {};
accounts
return () => {
return { account };
}; };
} }

View File

@ -28,7 +28,12 @@ export default class TransactionPendingForm extends Component {
isSending: PropTypes.bool.isRequired, isSending: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired,
className: PropTypes.string className: PropTypes.string,
focus: PropTypes.bool
};
static defaultProps = {
focus: false
}; };
state = { state = {
@ -47,7 +52,7 @@ export default class TransactionPendingForm extends Component {
} }
renderForm () { renderForm () {
const { address, isSending, onConfirm, onReject } = this.props; const { address, focus, isSending, onConfirm, onReject } = this.props;
if (this.state.isRejectOpen) { if (this.state.isRejectOpen) {
return ( return (
@ -59,8 +64,10 @@ export default class TransactionPendingForm extends Component {
return ( return (
<TransactionPendingFormConfirm <TransactionPendingFormConfirm
address={ address } address={ address }
focus={ focus }
isSending={ isSending } 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 { actions, gasLimit, isTest } = this.props;
const { date, id, isSending, payload } = data; const { date, id, isSending, payload } = data;
@ -86,6 +86,7 @@ class Embedded extends Component {
<RequestPending <RequestPending
className={ styles.request } className={ styles.request }
date={ date } date={ date }
focus={ index === 0 }
gasLimit={ gasLimit } gasLimit={ gasLimit }
id={ id } id={ id }
isSending={ isSending } isSending={ isSending }

View File

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

View File

@ -55,14 +55,20 @@ export default class WalletDetails extends Component {
return null; return null;
} }
const ownersList = owners.map((address, idx) => ( const ownersList = owners.map((owner, idx) => {
<InputAddress const address = typeof owner === 'object'
key={ `${idx}_${address}` } ? owner.address
value={ address } : owner;
disabled
text return (
/> <InputAddress
)); key={ `${idx}_${address}` }
value={ address }
disabled
text
/>
);
});
return ( return (
<div> <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; const { transactionHash, blockNumber, from, to, value, data } = transaction;
return ( return (
<TxRow <TxRow
key={ transactionHash } key={ `${transactionHash}_${index}` }
tx={ { tx={ {
hash: transactionHash, hash: transactionHash,
input: data && bytesToHex(data) || '', input: data && bytesToHex(data) || '',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -193,6 +193,7 @@ fn rpc_eth_logs() {
data: vec![1,2,3], data: vec![1,2,3],
}, },
transaction_index: 0, transaction_index: 0,
transaction_log_index: 0,
transaction_hash: H256::default(), transaction_hash: H256::default(),
log_index: 0, log_index: 0,
}, LocalizedLogEntry { }, LocalizedLogEntry {
@ -204,8 +205,9 @@ fn rpc_eth_logs() {
data: vec![1,2,3], data: vec![1,2,3],
}, },
transaction_index: 0, transaction_index: 0,
transaction_log_index: 1,
transaction_hash: H256::default(), 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 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 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 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":"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":"0x1","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x1","type":"mined"}],"id":1}"#;
let response3 = r#"{"jsonrpc":"2.0","result":[],"id":1}"#; let response3 = r#"{"jsonrpc":"2.0","result":[],"id":1}"#;
assert_eq!(tester.io.handle_request_sync(request1), Some(response1.to_owned())); 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], data: vec![1,2,3],
}, },
transaction_index: 0, transaction_index: 0,
transaction_log_index: 0,
transaction_hash: H256::default(), transaction_hash: H256::default(),
log_index: 0, log_index: 0,
}, LocalizedLogEntry { }, LocalizedLogEntry {
@ -246,8 +249,9 @@ fn rpc_logs_filter() {
data: vec![1,2,3], data: vec![1,2,3],
}, },
transaction_index: 0, transaction_index: 0,
transaction_log_index: 1,
transaction_hash: H256::default(), transaction_hash: H256::default(),
log_index: 0, log_index: 1,
}]); }]);
// Register filters first // 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_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 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 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":"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":"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_changes1), Some(response1.to_owned()));
assert_eq!(tester.io.handle_request_sync(request_changes2), Some(response2.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, block_number: 0x4510c,
transaction_hash: H256::new(), transaction_hash: H256::new(),
transaction_index: 0, transaction_index: 0,
transaction_log_index: 0,
log_index: 1, log_index: 1,
}], }],
log_bloom: 0.into(), log_bloom: 0.into(),
@ -967,7 +972,7 @@ fn rpc_eth_transaction_receipt() {
"params": ["0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"], "params": ["0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"],
"id": 1 "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())); assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
} }

View File

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

View File

@ -109,7 +109,7 @@ mod tests {
#[test] #[test]
fn receipt_serialization() { 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 { let receipt = Receipt {
transaction_hash: Some(0.into()), transaction_hash: Some(0.into()),
@ -130,6 +130,7 @@ mod tests {
block_number: Some(0x4510c.into()), block_number: Some(0x4510c.into()),
transaction_hash: Some(0.into()), transaction_hash: Some(0.into()),
transaction_index: Some(0.into()), transaction_index: Some(0.into()),
transaction_log_index: None,
log_index: Some(1.into()), log_index: Some(1.into()),
log_type: "mined".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. /// Export the standardmap module.
pub mod standardmap; pub mod standardmap;
/// Export the journal module.
pub mod journal;
/// Export the node module. /// Export the node module.
pub mod node; pub mod node;
/// Export the triedb module. /// Export the triedb module.

View File

@ -18,7 +18,6 @@ use elastic_array::ElasticArray36;
use nibbleslice::*; use nibbleslice::*;
use bytes::*; use bytes::*;
use rlp::*; use rlp::*;
use super::journal::*;
use hashdb::DBValue; use hashdb::DBValue;
/// Partial node key type. /// 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())
}
}
}
} }