Merge branch 'master' into on-demand-les-request
This commit is contained in:
commit
62bc92ff4d
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1503,7 +1503,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parity-ui-precompiled"
|
||||
version = "1.4.0"
|
||||
source = "git+https://github.com/ethcore/js-precompiled.git#d95a7dd2cc7469dc58af77743ec3ebc65e51cf36"
|
||||
source = "git+https://github.com/ethcore/js-precompiled.git#ebea2bf78e076916b51b04d8b24187a6a85ae440"
|
||||
dependencies = [
|
||||
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
@ -378,7 +378,8 @@ impl BlockProvider for BlockChain {
|
||||
.enumerate()
|
||||
.flat_map(move |(index, (mut logs, tx_hash))| {
|
||||
let current_log_index = log_index;
|
||||
log_index -= logs.len();
|
||||
let no_of_logs = logs.len();
|
||||
log_index -= no_of_logs;
|
||||
|
||||
logs.reverse();
|
||||
logs.into_iter()
|
||||
@ -390,6 +391,7 @@ impl BlockProvider for BlockChain {
|
||||
transaction_hash: tx_hash,
|
||||
// iterating in reverse order
|
||||
transaction_index: receipts_len - index - 1,
|
||||
transaction_log_index: no_of_logs - i - 1,
|
||||
log_index: current_log_index - i - 1,
|
||||
})
|
||||
})
|
||||
@ -1936,6 +1938,7 @@ mod tests {
|
||||
block_number: block1.header().number(),
|
||||
transaction_hash: tx_hash1.clone(),
|
||||
transaction_index: 0,
|
||||
transaction_log_index: 0,
|
||||
log_index: 0,
|
||||
},
|
||||
LocalizedLogEntry {
|
||||
@ -1944,6 +1947,7 @@ mod tests {
|
||||
block_number: block1.header().number(),
|
||||
transaction_hash: tx_hash1.clone(),
|
||||
transaction_index: 0,
|
||||
transaction_log_index: 1,
|
||||
log_index: 1,
|
||||
},
|
||||
LocalizedLogEntry {
|
||||
@ -1952,6 +1956,7 @@ mod tests {
|
||||
block_number: block1.header().number(),
|
||||
transaction_hash: tx_hash2.clone(),
|
||||
transaction_index: 1,
|
||||
transaction_log_index: 0,
|
||||
log_index: 2,
|
||||
},
|
||||
LocalizedLogEntry {
|
||||
@ -1960,6 +1965,7 @@ mod tests {
|
||||
block_number: block2.header().number(),
|
||||
transaction_hash: tx_hash3.clone(),
|
||||
transaction_index: 0,
|
||||
transaction_log_index: 0,
|
||||
log_index: 0,
|
||||
}
|
||||
]);
|
||||
@ -1970,6 +1976,7 @@ mod tests {
|
||||
block_number: block2.header().number(),
|
||||
transaction_hash: tx_hash3.clone(),
|
||||
transaction_index: 0,
|
||||
transaction_log_index: 0,
|
||||
log_index: 0,
|
||||
}
|
||||
]);
|
||||
|
@ -59,7 +59,7 @@ use client::{
|
||||
use client::Error as ClientError;
|
||||
use env_info::EnvInfo;
|
||||
use executive::{Executive, Executed, TransactOptions, contract_address};
|
||||
use receipt::LocalizedReceipt;
|
||||
use receipt::{Receipt, LocalizedReceipt};
|
||||
use trace::{TraceDB, ImportRequest as TraceImportRequest, LocalizedTrace, Database as TraceDatabase};
|
||||
use trace;
|
||||
use trace::FlatTransactionTraces;
|
||||
@ -837,7 +837,6 @@ impl snapshot::DatabaseRestore for Client {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl BlockChainClient for Client {
|
||||
fn call(&self, t: &SignedTransaction, block: BlockId, analytics: CallAnalytics) -> Result<Executed, CallError> {
|
||||
let header = self.block_header(block).ok_or(CallError::StatePruned)?;
|
||||
@ -1134,51 +1133,21 @@ impl BlockChainClient for Client {
|
||||
let chain = self.chain.read();
|
||||
self.transaction_address(id)
|
||||
.and_then(|address| chain.block_number(&address.block_hash).and_then(|block_number| {
|
||||
let t = chain.block_body(&address.block_hash)
|
||||
.and_then(|body| {
|
||||
body.view().localized_transaction_at(&address.block_hash, block_number, address.index)
|
||||
});
|
||||
let transaction = chain.block_body(&address.block_hash)
|
||||
.and_then(|body| body.view().localized_transaction_at(&address.block_hash, block_number, address.index));
|
||||
|
||||
let tx_and_sender = t.and_then(|tx| tx.sender().ok().map(|sender| (tx, sender)));
|
||||
|
||||
match (tx_and_sender, chain.transaction_receipt(&address)) {
|
||||
(Some((tx, sender)), Some(receipt)) => {
|
||||
let block_hash = tx.block_hash.clone();
|
||||
let block_number = tx.block_number.clone();
|
||||
let transaction_hash = tx.hash();
|
||||
let transaction_index = tx.transaction_index;
|
||||
let prior_gas_used = match tx.transaction_index {
|
||||
0 => U256::zero(),
|
||||
i => {
|
||||
let prior_address = TransactionAddress { block_hash: address.block_hash, index: i - 1 };
|
||||
let prior_receipt = chain.transaction_receipt(&prior_address).expect("Transaction receipt at `address` exists; `prior_address` has lower index in same block; qed");
|
||||
prior_receipt.gas_used
|
||||
}
|
||||
};
|
||||
Some(LocalizedReceipt {
|
||||
transaction_hash: tx.hash(),
|
||||
transaction_index: tx.transaction_index,
|
||||
block_hash: tx.block_hash,
|
||||
block_number: tx.block_number,
|
||||
cumulative_gas_used: receipt.gas_used,
|
||||
gas_used: receipt.gas_used - prior_gas_used,
|
||||
contract_address: match tx.action {
|
||||
Action::Call(_) => None,
|
||||
Action::Create => Some(contract_address(&sender, &tx.nonce))
|
||||
},
|
||||
logs: receipt.logs.into_iter().enumerate().map(|(i, log)| LocalizedLogEntry {
|
||||
entry: log,
|
||||
block_hash: block_hash.clone(),
|
||||
block_number: block_number,
|
||||
transaction_hash: transaction_hash.clone(),
|
||||
transaction_index: transaction_index,
|
||||
log_index: i
|
||||
}).collect(),
|
||||
log_bloom: receipt.log_bloom,
|
||||
state_root: receipt.state_root,
|
||||
let previous_receipts = (0..address.index + 1)
|
||||
.map(|index| {
|
||||
let mut address = address.clone();
|
||||
address.index = index;
|
||||
chain.transaction_receipt(&address)
|
||||
})
|
||||
.collect();
|
||||
match (transaction, previous_receipts) {
|
||||
(Some(transaction), Some(previous_receipts)) => {
|
||||
Some(transaction_receipt(transaction, previous_receipts))
|
||||
},
|
||||
_ => None
|
||||
_ => None,
|
||||
}
|
||||
}))
|
||||
}
|
||||
@ -1535,6 +1504,49 @@ impl Drop for Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `LocalizedReceipt` given `LocalizedTransaction`
|
||||
/// and a vector of receipts from given block up to transaction index.
|
||||
fn transaction_receipt(tx: LocalizedTransaction, mut receipts: Vec<Receipt>) -> LocalizedReceipt {
|
||||
assert_eq!(receipts.len(), tx.transaction_index + 1, "All previous receipts are provided.");
|
||||
|
||||
let sender = tx.sender()
|
||||
.expect("LocalizedTransaction is part of the blockchain; We have only valid transactions in chain; qed");
|
||||
let receipt = receipts.pop().expect("Current receipt is provided; qed");
|
||||
let prior_gas_used = match tx.transaction_index {
|
||||
0 => 0.into(),
|
||||
i => receipts.get(i - 1).expect("All previous receipts are provided; qed").gas_used,
|
||||
};
|
||||
let no_of_logs = receipts.into_iter().map(|receipt| receipt.logs.len()).sum::<usize>();
|
||||
let transaction_hash = tx.hash();
|
||||
let block_hash = tx.block_hash;
|
||||
let block_number = tx.block_number;
|
||||
let transaction_index = tx.transaction_index;
|
||||
|
||||
LocalizedReceipt {
|
||||
transaction_hash: transaction_hash,
|
||||
transaction_index: transaction_index,
|
||||
block_hash: block_hash,
|
||||
block_number:block_number,
|
||||
cumulative_gas_used: receipt.gas_used,
|
||||
gas_used: receipt.gas_used - prior_gas_used,
|
||||
contract_address: match tx.action {
|
||||
Action::Call(_) => None,
|
||||
Action::Create => Some(contract_address(&sender, &tx.nonce))
|
||||
},
|
||||
logs: receipt.logs.into_iter().enumerate().map(|(i, log)| LocalizedLogEntry {
|
||||
entry: log,
|
||||
block_hash: block_hash,
|
||||
block_number: block_number,
|
||||
transaction_hash: transaction_hash,
|
||||
transaction_index: transaction_index,
|
||||
transaction_log_index: i,
|
||||
log_index: no_of_logs + i,
|
||||
}).collect(),
|
||||
log_bloom: receipt.log_bloom,
|
||||
state_root: receipt.state_root,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@ -1570,4 +1582,91 @@ mod tests {
|
||||
|
||||
assert!(client.tree_route(&genesis, &new_hash).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_correct_log_index() {
|
||||
use super::transaction_receipt;
|
||||
use ethkey::KeyPair;
|
||||
use log_entry::{LogEntry, LocalizedLogEntry};
|
||||
use receipt::{Receipt, LocalizedReceipt};
|
||||
use transaction::{Transaction, LocalizedTransaction, Action};
|
||||
use util::Hashable;
|
||||
|
||||
// given
|
||||
let key = KeyPair::from_secret("test".sha3()).unwrap();
|
||||
let secret = key.secret();
|
||||
|
||||
let block_number = 1;
|
||||
let block_hash = 5.into();
|
||||
let state_root = 99.into();
|
||||
let gas_used = 10.into();
|
||||
let raw_tx = Transaction {
|
||||
nonce: 0.into(),
|
||||
gas_price: 0.into(),
|
||||
gas: 21000.into(),
|
||||
action: Action::Call(10.into()),
|
||||
value: 0.into(),
|
||||
data: vec![],
|
||||
};
|
||||
let tx1 = raw_tx.clone().sign(secret, None);
|
||||
let transaction = LocalizedTransaction {
|
||||
signed: tx1.clone(),
|
||||
block_number: block_number,
|
||||
block_hash: block_hash,
|
||||
transaction_index: 1,
|
||||
};
|
||||
let logs = vec![LogEntry {
|
||||
address: 5.into(),
|
||||
topics: vec![],
|
||||
data: vec![],
|
||||
}, LogEntry {
|
||||
address: 15.into(),
|
||||
topics: vec![],
|
||||
data: vec![],
|
||||
}];
|
||||
let receipts = vec![Receipt {
|
||||
state_root: state_root,
|
||||
gas_used: 5.into(),
|
||||
log_bloom: Default::default(),
|
||||
logs: vec![logs[0].clone()],
|
||||
}, Receipt {
|
||||
state_root: state_root,
|
||||
gas_used: gas_used,
|
||||
log_bloom: Default::default(),
|
||||
logs: logs.clone(),
|
||||
}];
|
||||
|
||||
// when
|
||||
let receipt = transaction_receipt(transaction, receipts);
|
||||
|
||||
// then
|
||||
assert_eq!(receipt, LocalizedReceipt {
|
||||
transaction_hash: tx1.hash(),
|
||||
transaction_index: 1,
|
||||
block_hash: block_hash,
|
||||
block_number: block_number,
|
||||
cumulative_gas_used: gas_used,
|
||||
gas_used: gas_used - 5.into(),
|
||||
contract_address: None,
|
||||
logs: vec![LocalizedLogEntry {
|
||||
entry: logs[0].clone(),
|
||||
block_hash: block_hash,
|
||||
block_number: block_number,
|
||||
transaction_hash: tx1.hash(),
|
||||
transaction_index: 1,
|
||||
transaction_log_index: 0,
|
||||
log_index: 1,
|
||||
}, LocalizedLogEntry {
|
||||
entry: logs[1].clone(),
|
||||
block_hash: block_hash,
|
||||
block_number: block_number,
|
||||
transaction_hash: tx1.hash(),
|
||||
transaction_index: 1,
|
||||
transaction_log_index: 1,
|
||||
log_index: 2,
|
||||
}],
|
||||
log_bloom: Default::default(),
|
||||
state_root: state_root,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -320,4 +320,3 @@ fn does_not_propagate_delayed_transactions() {
|
||||
assert_eq!(2, client.ready_transactions().len());
|
||||
assert_eq!(2, client.miner().pending_transactions().len());
|
||||
}
|
||||
|
||||
|
@ -97,6 +97,8 @@ pub struct LocalizedLogEntry {
|
||||
pub transaction_index: usize,
|
||||
/// Log position in the block.
|
||||
pub log_index: usize,
|
||||
/// Log position in the transaction.
|
||||
pub transaction_log_index: usize,
|
||||
}
|
||||
|
||||
impl Deref for LocalizedLogEntry {
|
||||
|
@ -373,7 +373,7 @@ impl SignedTransaction {
|
||||
}
|
||||
|
||||
/// Signed Transaction that is a part of canon blockchain.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "ipc", binary)]
|
||||
pub struct LocalizedTransaction {
|
||||
/// Signed part.
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parity.js",
|
||||
"version": "0.2.152",
|
||||
"version": "0.2.165",
|
||||
"main": "release/index.js",
|
||||
"jsnext:main": "src/index.js",
|
||||
"author": "Parity Team <admin@parity.io>",
|
||||
@ -26,9 +26,9 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build:lib && npm run build:dll && npm run build:app",
|
||||
"build:app": "webpack --config webpack/app --progress",
|
||||
"build:lib": "webpack --config webpack/libraries --progress",
|
||||
"build:dll": "webpack --config webpack/vendor --progress",
|
||||
"build:app": "webpack --config webpack/app",
|
||||
"build:lib": "webpack --config webpack/libraries",
|
||||
"build:dll": "webpack --config webpack/vendor",
|
||||
"ci:build": "npm run ci:build:lib && npm run ci:build:dll && npm run ci:build:app",
|
||||
"ci:build:app": "NODE_ENV=production webpack --config webpack/app",
|
||||
"ci:build:lib": "NODE_ENV=production webpack --config webpack/libraries",
|
||||
@ -51,19 +51,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "6.18.0",
|
||||
"babel-core": "6.20.0",
|
||||
"babel-core": "6.21.0",
|
||||
"babel-eslint": "7.1.1",
|
||||
"babel-loader": "6.2.10",
|
||||
"babel-plugin-lodash": "3.2.10",
|
||||
"babel-plugin-lodash": "3.2.11",
|
||||
"babel-plugin-react-intl": "2.2.0",
|
||||
"babel-plugin-transform-class-properties": "6.18.0",
|
||||
"babel-plugin-transform-class-properties": "6.19.0",
|
||||
"babel-plugin-transform-decorators-legacy": "1.3.4",
|
||||
"babel-plugin-transform-object-rest-spread": "6.20.2",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.2.11",
|
||||
"babel-plugin-transform-runtime": "6.15.0",
|
||||
"babel-plugin-webpack-alias": "2.1.2",
|
||||
"babel-polyfill": "6.20.0",
|
||||
"babel-preset-env": "1.0.2",
|
||||
"babel-preset-env": "1.1.4",
|
||||
"babel-preset-es2015": "6.18.0",
|
||||
"babel-preset-es2016": "6.16.0",
|
||||
"babel-preset-es2017": "6.16.0",
|
||||
@ -80,57 +80,58 @@
|
||||
"coveralls": "2.11.15",
|
||||
"css-loader": "0.26.1",
|
||||
"ejs-loader": "0.3.0",
|
||||
"enzyme": "2.6.0",
|
||||
"enzyme": "2.7.0",
|
||||
"eslint": "3.11.1",
|
||||
"eslint-config-semistandard": "7.0.0",
|
||||
"eslint-config-standard": "6.2.1",
|
||||
"eslint-config-standard-react": "4.2.0",
|
||||
"eslint-plugin-promise": "3.4.0",
|
||||
"eslint-plugin-react": "6.7.1",
|
||||
"eslint-plugin-react": "6.8.0",
|
||||
"eslint-plugin-standard": "2.0.1",
|
||||
"express": "4.14.0",
|
||||
"extract-loader": "0.1.0",
|
||||
"extract-text-webpack-plugin": "2.0.0-beta.4",
|
||||
"file-loader": "0.9.0",
|
||||
"happypack": "3.0.0",
|
||||
"happypack": "3.0.2",
|
||||
"html-loader": "0.4.4",
|
||||
"html-webpack-plugin": "2.24.1",
|
||||
"http-proxy-middleware": "0.17.2",
|
||||
"http-proxy-middleware": "0.17.3",
|
||||
"husky": "0.11.9",
|
||||
"ignore-styles": "5.0.1",
|
||||
"image-webpack-loader": "3.0.0",
|
||||
"image-webpack-loader": "3.1.0",
|
||||
"istanbul": "1.0.0-alpha.2",
|
||||
"jsdom": "9.8.3",
|
||||
"jsdom": "9.9.1",
|
||||
"json-loader": "0.5.4",
|
||||
"mocha": "3.2.0",
|
||||
"mock-local-storage": "1.0.2",
|
||||
"mock-socket": "6.0.3",
|
||||
"mock-socket": "6.0.4",
|
||||
"nock": "9.0.2",
|
||||
"postcss-import": "9.0.0",
|
||||
"postcss-loader": "1.2.0",
|
||||
"postcss-loader": "1.2.1",
|
||||
"postcss-nested": "1.0.0",
|
||||
"postcss-simple-vars": "3.0.0",
|
||||
"progress": "1.1.8",
|
||||
"progress-bar-webpack-plugin": "1.9.1",
|
||||
"raw-loader": "0.5.1",
|
||||
"react-addons-perf": "15.4.1",
|
||||
"react-addons-test-utils": "15.4.1",
|
||||
"react-hot-loader": "3.0.0-beta.6",
|
||||
"react-intl-aggregate-webpack-plugin": "0.0.1",
|
||||
"rucksack-css": "0.9.1",
|
||||
"script-ext-html-webpack-plugin": "1.3.4",
|
||||
"script-ext-html-webpack-plugin": "1.3.5",
|
||||
"serviceworker-webpack-plugin": "0.1.7",
|
||||
"sinon": "1.17.6",
|
||||
"sinon-as-promised": "4.0.2",
|
||||
"sinon-chai": "2.8.0",
|
||||
"style-loader": "0.13.1",
|
||||
"stylelint": "7.6.0",
|
||||
"stylelint-config-standard": "15.0.0",
|
||||
"stylelint": "7.7.0",
|
||||
"stylelint-config-standard": "15.0.1",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "2.2.0-rc.2",
|
||||
"webpack-dev-middleware": "1.8.4",
|
||||
"webpack-dev-middleware": "1.9.0",
|
||||
"webpack-error-notification": "0.1.6",
|
||||
"webpack-hot-middleware": "2.13.2",
|
||||
"websocket": "1.0.23"
|
||||
"webpack-hot-middleware": "2.14.0",
|
||||
"websocket": "1.0.24"
|
||||
},
|
||||
"dependencies": {
|
||||
"bignumber.js": "3.0.1",
|
||||
|
@ -25,7 +25,7 @@ export default class Encoder {
|
||||
throw new Error('tokens should be array of Token');
|
||||
}
|
||||
|
||||
const mediates = tokens.map((token) => Encoder.encodeToken(token));
|
||||
const mediates = tokens.map((token, index) => Encoder.encodeToken(token, index));
|
||||
const inits = mediates
|
||||
.map((mediate, idx) => mediate.init(Mediate.offsetFor(mediates, idx)))
|
||||
.join('');
|
||||
@ -36,11 +36,12 @@ export default class Encoder {
|
||||
return `${inits}${closings}`;
|
||||
}
|
||||
|
||||
static encodeToken (token) {
|
||||
static encodeToken (token, index = 0) {
|
||||
if (!isInstanceOf(token, Token)) {
|
||||
throw new Error('token should be instanceof Token');
|
||||
}
|
||||
|
||||
try {
|
||||
switch (token.type) {
|
||||
case 'address':
|
||||
return new Mediate('raw', padAddress(token.value));
|
||||
@ -64,9 +65,11 @@ export default class Encoder {
|
||||
case 'fixedArray':
|
||||
case 'array':
|
||||
return new Mediate(token.type, token.value.map((token) => Encoder.encodeToken(token)));
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`Cannot encode token #${index} [${token.type}: ${token.value}]. ${e.message}`);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid token type ${token.type} in encodeToken`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,10 @@ export default class Interface {
|
||||
}
|
||||
|
||||
encodeTokens (paramTypes, values) {
|
||||
return Interface.encodeTokens(paramTypes, values);
|
||||
}
|
||||
|
||||
static encodeTokens (paramTypes, values) {
|
||||
const createToken = function (paramType, value) {
|
||||
if (paramType.subtype) {
|
||||
return new Token(paramType.type, value.map((entry) => createToken(paramType.subtype, entry)));
|
||||
|
@ -114,7 +114,11 @@ export default class Api {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// Don't print if the request is rejected: that's ok
|
||||
if (error.type !== 'REQUEST_REJECTED') {
|
||||
console.error('pollMethod', error);
|
||||
}
|
||||
|
||||
reject(error);
|
||||
});
|
||||
};
|
||||
|
@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Abi from '../../abi';
|
||||
import Abi from '~/abi';
|
||||
|
||||
let nextSubscriptionId = 0;
|
||||
|
||||
@ -53,6 +53,10 @@ export default class Contract {
|
||||
|
||||
this._subscribedToBlock = false;
|
||||
this._blockSubscriptionId = null;
|
||||
|
||||
if (api && api.patch && api.patch.contract) {
|
||||
api.patch.contract(this);
|
||||
}
|
||||
}
|
||||
|
||||
get address () {
|
||||
@ -90,8 +94,10 @@ export default class Contract {
|
||||
}
|
||||
|
||||
deployEstimateGas (options, values) {
|
||||
const _options = this._encodeOptions(this.constructors[0], options, values);
|
||||
|
||||
return this._api.eth
|
||||
.estimateGas(this._encodeOptions(this.constructors[0], options, values))
|
||||
.estimateGas(_options)
|
||||
.then((gasEst) => {
|
||||
return [gasEst, gasEst.mul(1.2)];
|
||||
});
|
||||
@ -115,8 +121,10 @@ export default class Contract {
|
||||
|
||||
setState({ state: 'postTransaction', gas });
|
||||
|
||||
const _options = this._encodeOptions(this.constructors[0], options, values);
|
||||
|
||||
return this._api.parity
|
||||
.postTransaction(this._encodeOptions(this.constructors[0], options, values))
|
||||
.postTransaction(_options)
|
||||
.then((requestId) => {
|
||||
setState({ state: 'checkRequest', requestId });
|
||||
return this._pollCheckRequest(requestId);
|
||||
@ -199,7 +207,7 @@ export default class Contract {
|
||||
getCallData = (func, options, values) => {
|
||||
let data = options.data;
|
||||
|
||||
const tokens = func ? this._abi.encodeTokens(func.inputParamTypes(), values) : null;
|
||||
const tokens = func ? Abi.encodeTokens(func.inputParamTypes(), values) : null;
|
||||
const call = tokens ? func.encodeCall(tokens) : null;
|
||||
|
||||
if (data && data.substr(0, 2) === '0x') {
|
||||
@ -221,6 +229,8 @@ export default class Contract {
|
||||
}
|
||||
|
||||
_bindFunction = (func) => {
|
||||
func.contract = this;
|
||||
|
||||
func.call = (options, values = []) => {
|
||||
const callParams = this._encodeOptions(func, this._addOptionsTo(options), values);
|
||||
|
||||
@ -233,13 +243,13 @@ export default class Contract {
|
||||
|
||||
if (!func.constant) {
|
||||
func.postTransaction = (options, values = []) => {
|
||||
return this._api.parity
|
||||
.postTransaction(this._encodeOptions(func, this._addOptionsTo(options), values));
|
||||
const _options = this._encodeOptions(func, this._addOptionsTo(options), values);
|
||||
return this._api.parity.postTransaction(_options);
|
||||
};
|
||||
|
||||
func.estimateGas = (options, values = []) => {
|
||||
return this._api.eth
|
||||
.estimateGas(this._encodeOptions(func, this._addOptionsTo(options), values));
|
||||
const _options = this._encodeOptions(func, this._addOptionsTo(options), values);
|
||||
return this._api.eth.estimateGas(_options);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -209,7 +209,10 @@ export default class Ws extends JsonRpcBase {
|
||||
if (result.error) {
|
||||
this.error(event.data);
|
||||
|
||||
// Don't print error if request rejected...
|
||||
if (!/rejected/.test(result.error.message)) {
|
||||
console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`);
|
||||
}
|
||||
|
||||
const error = new TransportError(method, result.error.code, result.error.message);
|
||||
reject(error);
|
||||
|
@ -47,8 +47,6 @@ export function decodeMethodInput (methodAbi, paramdata) {
|
||||
throw new Error('Input to decodeMethodInput should be a hex value');
|
||||
} else if (paramdata.substr(0, 2) === '0x') {
|
||||
return decodeMethodInput(methodAbi, paramdata.slice(2));
|
||||
} else if (paramdata.length % 64 !== 0) {
|
||||
throw new Error('Parameter length in decodeMethodInput not a multiple of 64 characters');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,10 +48,6 @@ describe('api/util/decode', () => {
|
||||
expect(() => decodeMethodInput({}, 'invalid')).to.throw(/should be a hex value/);
|
||||
});
|
||||
|
||||
it('throws on invalid lengths', () => {
|
||||
expect(() => decodeMethodInput({}, DATA.slice(-32))).to.throw(/not a multiple of/);
|
||||
});
|
||||
|
||||
it('correctly decodes valid inputs', () => {
|
||||
expect(decodeMethodInput({
|
||||
type: 'function',
|
||||
|
@ -36,6 +36,7 @@ import ContextProvider from '~/ui/ContextProvider';
|
||||
import muiTheme from '~/ui/Theme';
|
||||
import MainApplication from './main';
|
||||
|
||||
import { patchApi } from '~/util/tx';
|
||||
import { setApi } from '~/redux/providers/apiActions';
|
||||
|
||||
import './environment';
|
||||
@ -60,6 +61,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
|
||||
}
|
||||
|
||||
const api = new SecureApi(`ws://${parityUrl}`, token);
|
||||
patchApi(api);
|
||||
ContractInstances.create(api);
|
||||
|
||||
const store = initStore(api, hashHistory);
|
||||
|
@ -14,264 +14,255 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ContentAdd from 'material-ui/svg-icons/content/add';
|
||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
||||
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
|
||||
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { newError } from '~/redux/actions';
|
||||
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
|
||||
import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation';
|
||||
import { AddIcon, CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
|
||||
|
||||
import { eip20, wallet } from '~/contracts/abi';
|
||||
import Store from './store';
|
||||
|
||||
const ABI_TYPES = [
|
||||
{
|
||||
label: 'Token', readOnly: true, value: JSON.stringify(eip20),
|
||||
type: 'token',
|
||||
description: (<span>A standard <a href='https://github.com/ethereum/EIPs/issues/20' target='_blank'>ERC 20</a> token</span>)
|
||||
},
|
||||
{
|
||||
label: 'Multisig Wallet', readOnly: true,
|
||||
type: 'multisig',
|
||||
value: JSON.stringify(wallet),
|
||||
description: (<span>Official Multisig contract: <a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>see contract code</a></span>)
|
||||
},
|
||||
{
|
||||
label: 'Custom Contract', value: '',
|
||||
type: 'custom',
|
||||
description: 'Contract created from custom ABI'
|
||||
}
|
||||
];
|
||||
|
||||
const STEPS = [ 'choose a contract type', 'enter contract details' ];
|
||||
|
||||
export default class AddContract extends Component {
|
||||
@observer
|
||||
class AddContract extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
contracts: PropTypes.object.isRequired,
|
||||
newError: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
|
||||
state = {
|
||||
abi: '',
|
||||
abiError: ERRORS.invalidAbi,
|
||||
abiType: ABI_TYPES[2],
|
||||
abiTypeIndex: 2,
|
||||
abiParsed: null,
|
||||
address: '',
|
||||
addressError: ERRORS.invalidAddress,
|
||||
name: '',
|
||||
nameError: ERRORS.invalidName,
|
||||
description: '',
|
||||
step: 0
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.onChangeABIType(null, this.state.abiTypeIndex);
|
||||
}
|
||||
store = new Store(this.context.api, this.props.contracts);
|
||||
|
||||
render () {
|
||||
const { step } = this.state;
|
||||
const { step } = this.store;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible
|
||||
actions={ this.renderDialogActions() }
|
||||
steps={ STEPS }
|
||||
current={ step }
|
||||
>
|
||||
{ this.renderStep(step) }
|
||||
steps={ [
|
||||
<FormattedMessage
|
||||
id='addContract.title.type'
|
||||
defaultMessage='choose a contract type'
|
||||
key='type' />,
|
||||
<FormattedMessage
|
||||
id='addContract.title.details'
|
||||
defaultMessage='enter contract details'
|
||||
key='details' />
|
||||
] }
|
||||
visible>
|
||||
{ this.renderStep() }
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderStep (step) {
|
||||
renderStep () {
|
||||
const { step } = this.store;
|
||||
|
||||
switch (step) {
|
||||
case 0:
|
||||
return this.renderContractTypeSelector();
|
||||
|
||||
default:
|
||||
return this.renderFields();
|
||||
}
|
||||
}
|
||||
|
||||
renderContractTypeSelector () {
|
||||
const { abiTypeIndex } = this.state;
|
||||
const { abiTypeIndex, abiTypes } = this.store;
|
||||
|
||||
return (
|
||||
<RadioButtons
|
||||
name='contractType'
|
||||
value={ abiTypeIndex }
|
||||
values={ this.getAbiTypes() }
|
||||
values={ abiTypes }
|
||||
onChange={ this.onChangeABIType }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderDialogActions () {
|
||||
const { addressError, nameError, step } = this.state;
|
||||
const hasError = !!(addressError || nameError);
|
||||
const { step } = this.store;
|
||||
|
||||
const cancelBtn = (
|
||||
<Button
|
||||
icon={ <ContentClear /> }
|
||||
label='Cancel'
|
||||
icon={ <CancelIcon /> }
|
||||
key='cancel'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='addContract.button.cancel'
|
||||
defaultMessage='Cancel' />
|
||||
}
|
||||
onClick={ this.onClose } />
|
||||
);
|
||||
|
||||
if (step === 0) {
|
||||
const nextBtn = (
|
||||
return [
|
||||
cancelBtn,
|
||||
<Button
|
||||
icon={ <NavigationArrowForward /> }
|
||||
label='Next'
|
||||
icon={ <NextIcon /> }
|
||||
key='next'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='addContract.button.next'
|
||||
defaultMessage='Next' />
|
||||
}
|
||||
onClick={ this.onNext } />
|
||||
);
|
||||
|
||||
return [ cancelBtn, nextBtn ];
|
||||
];
|
||||
}
|
||||
|
||||
const prevBtn = (
|
||||
return [
|
||||
cancelBtn,
|
||||
<Button
|
||||
icon={ <NavigationArrowBack /> }
|
||||
label='Back'
|
||||
onClick={ this.onPrev } />
|
||||
);
|
||||
|
||||
const addBtn = (
|
||||
icon={ <PrevIcon /> }
|
||||
key='prev'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='addContract.button.prev'
|
||||
defaultMessage='Back' />
|
||||
}
|
||||
onClick={ this.onPrev } />,
|
||||
<Button
|
||||
icon={ <ContentAdd /> }
|
||||
label='Add Contract'
|
||||
disabled={ hasError }
|
||||
icon={ <AddIcon /> }
|
||||
key='add'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='addContract.button.add'
|
||||
defaultMessage='Add Contract' />
|
||||
}
|
||||
disabled={ this.store.hasError }
|
||||
onClick={ this.onAdd } />
|
||||
);
|
||||
|
||||
return [ cancelBtn, prevBtn, addBtn ];
|
||||
];
|
||||
}
|
||||
|
||||
renderFields () {
|
||||
const { abi, abiError, address, addressError, description, name, nameError, abiType } = this.state;
|
||||
const { abi, abiError, abiType, address, addressError, description, name, nameError } = this.store;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<InputAddress
|
||||
label='network address'
|
||||
hint='the network address for the contract'
|
||||
error={ addressError }
|
||||
value={ address }
|
||||
onSubmit={ this.onEditAddress }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='addContract.address.hint'
|
||||
defaultMessage='the network address for the contract' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='addContract.address.label'
|
||||
defaultMessage='network address' />
|
||||
}
|
||||
onChange={ this.onChangeAddress }
|
||||
/>
|
||||
onSubmit={ this.onEditAddress }
|
||||
value={ address } />
|
||||
<Input
|
||||
label='contract name'
|
||||
hint='a descriptive name for the contract'
|
||||
error={ nameError }
|
||||
value={ name }
|
||||
onSubmit={ this.onEditName } />
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='addContract.name.hint'
|
||||
defaultMessage='a descriptive name for the contract' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='addContract.name.label'
|
||||
defaultMessage='contract name' />
|
||||
}
|
||||
onSubmit={ this.onEditName }
|
||||
value={ name } />
|
||||
<Input
|
||||
multiLine
|
||||
rows={ 1 }
|
||||
label='(optional) contract description'
|
||||
hint='an expanded description for the entry'
|
||||
value={ description }
|
||||
onSubmit={ this.onEditDescription } />
|
||||
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='addContract.description.hint'
|
||||
defaultMessage='an expanded description for the entry' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='addContract.description.label'
|
||||
defaultMessage='(optional) contract description' />
|
||||
}
|
||||
onSubmit={ this.onEditDescription }
|
||||
value={ description } />
|
||||
<Input
|
||||
label='contract abi'
|
||||
hint='the abi for the contract'
|
||||
error={ abiError }
|
||||
value={ abi }
|
||||
readOnly={ abiType.readOnly }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='addContract.abi.hint'
|
||||
defaultMessage='the abi for the contract' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='addContract.abi.label'
|
||||
defaultMessage='contract abi' />
|
||||
}
|
||||
onSubmit={ this.onEditAbi }
|
||||
/>
|
||||
readOnly={ abiType.readOnly }
|
||||
value={ abi } />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
getAbiTypes () {
|
||||
return ABI_TYPES.map((type, index) => ({
|
||||
label: type.label,
|
||||
description: type.description,
|
||||
key: index,
|
||||
...type
|
||||
}));
|
||||
}
|
||||
|
||||
onNext = () => {
|
||||
this.setState({ step: this.state.step + 1 });
|
||||
this.store.nextStep();
|
||||
}
|
||||
|
||||
onPrev = () => {
|
||||
this.setState({ step: this.state.step - 1 });
|
||||
this.store.prevStep();
|
||||
}
|
||||
|
||||
onChangeABIType = (value, index) => {
|
||||
const abiType = value || ABI_TYPES[index];
|
||||
this.setState({ abiTypeIndex: index, abiType });
|
||||
this.onEditAbi(abiType.value);
|
||||
this.store.setAbiTypeIndex(index);
|
||||
}
|
||||
|
||||
onEditAbi = (abiIn) => {
|
||||
const { api } = this.context;
|
||||
const { abi, abiError, abiParsed } = validateAbi(abiIn, api);
|
||||
|
||||
this.setState({ abi, abiError, abiParsed });
|
||||
onEditAbi = (abi) => {
|
||||
this.store.setAbi(abi);
|
||||
}
|
||||
|
||||
onChangeAddress = (event, value) => {
|
||||
this.onEditAddress(value);
|
||||
onChangeAddress = (event, address) => {
|
||||
this.onEditAddress(address);
|
||||
}
|
||||
|
||||
onEditAddress = (_address) => {
|
||||
const { contracts } = this.props;
|
||||
let { address, addressError } = validateAddress(_address);
|
||||
|
||||
if (!addressError) {
|
||||
const contract = contracts[address];
|
||||
|
||||
if (contract) {
|
||||
addressError = ERRORS.duplicateAddress;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
address,
|
||||
addressError
|
||||
});
|
||||
onEditAddress = (address) => {
|
||||
this.store.setAddress(address);
|
||||
}
|
||||
|
||||
onEditDescription = (description) => {
|
||||
this.setState({ description });
|
||||
this.store.setDescription(description);
|
||||
}
|
||||
|
||||
onEditName = (name) => {
|
||||
this.setState(validateName(name));
|
||||
this.store.setName(name);
|
||||
}
|
||||
|
||||
onAdd = () => {
|
||||
const { api } = this.context;
|
||||
const { abiParsed, address, name, description, abiType } = this.state;
|
||||
|
||||
Promise.all([
|
||||
api.parity.setAccountName(address, name),
|
||||
api.parity.setAccountMeta(address, {
|
||||
contract: true,
|
||||
deleted: false,
|
||||
timestamp: Date.now(),
|
||||
abi: abiParsed,
|
||||
type: abiType.type,
|
||||
description
|
||||
return this.store
|
||||
.addContract()
|
||||
.then(() => {
|
||||
this.onClose();
|
||||
})
|
||||
]).catch((error) => {
|
||||
console.error('onAdd', error);
|
||||
.catch((error) => {
|
||||
this.props.newError(error);
|
||||
});
|
||||
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return bindActionCreators({
|
||||
newError
|
||||
}, dispatch);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(AddContract);
|
||||
|
84
js/src/modals/AddContract/addContract.spec.js
Normal file
84
js/src/modals/AddContract/addContract.spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
49
js/src/modals/AddContract/addContract.test.js
Normal file
49
js/src/modals/AddContract/addContract.test.js
Normal 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
|
||||
};
|
126
js/src/modals/AddContract/store.js
Normal file
126
js/src/modals/AddContract/store.js
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
171
js/src/modals/AddContract/store.spec.js
Normal file
171
js/src/modals/AddContract/store.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
89
js/src/modals/AddContract/types.js
Normal file
89
js/src/modals/AddContract/types.js
Normal 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
|
||||
};
|
@ -18,28 +18,33 @@ import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default {
|
||||
noFile:
|
||||
noFile: (
|
||||
<FormattedMessage
|
||||
id='createAccount.error.noFile'
|
||||
defaultMessage='select a valid wallet file to import' />,
|
||||
defaultMessage='select a valid wallet file to import' />
|
||||
),
|
||||
|
||||
noKey:
|
||||
noKey: (
|
||||
<FormattedMessage
|
||||
id='createAccount.error.noKey'
|
||||
defaultMessage='you need to provide the raw private key' />,
|
||||
defaultMessage='you need to provide the raw private key' />
|
||||
),
|
||||
|
||||
noMatchPassword:
|
||||
noMatchPassword: (
|
||||
<FormattedMessage
|
||||
id='createAccount.error.noMatchPassword'
|
||||
defaultMessage='the supplied passwords does not match' />,
|
||||
defaultMessage='the supplied passwords does not match' />
|
||||
),
|
||||
|
||||
noName:
|
||||
noName: (
|
||||
<FormattedMessage
|
||||
id='createAccount.error.noName'
|
||||
defaultMessage='you need to specify a valid name for the account' />,
|
||||
defaultMessage='you need to specify a valid name for the account' />
|
||||
),
|
||||
|
||||
invalidKey:
|
||||
invalidKey: (
|
||||
<FormattedMessage
|
||||
id='createAccount.error.invalidKey'
|
||||
defaultMessage='the raw key needs to be hex, 64 characters in length and contain the prefix "0x"' />
|
||||
)
|
||||
};
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { omitBy } from 'lodash';
|
||||
|
||||
import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui';
|
||||
|
||||
@ -73,6 +74,9 @@ export default class WalletDetails extends Component {
|
||||
renderMultisigDetails () {
|
||||
const { accounts, wallet, errors } = this.props;
|
||||
|
||||
// Wallets cannot create contracts
|
||||
const _accounts = omitBy(accounts, (a) => a.wallet);
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<AddressSelect
|
||||
@ -81,7 +85,7 @@ export default class WalletDetails extends Component {
|
||||
value={ wallet.account }
|
||||
error={ errors.account }
|
||||
onChange={ this.onAccoutChange }
|
||||
accounts={ accounts }
|
||||
accounts={ _accounts }
|
||||
/>
|
||||
|
||||
<Input
|
||||
|
@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { pick } from 'lodash';
|
||||
import { pick, omitBy } from 'lodash';
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
@ -34,29 +34,33 @@ import { ERROR_CODES } from '~/api/transport/error';
|
||||
|
||||
const STEPS = {
|
||||
CONTRACT_DETAILS: {
|
||||
title:
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='deployContract.title.details'
|
||||
defaultMessage='contract details' />
|
||||
)
|
||||
},
|
||||
CONTRACT_PARAMETERS: {
|
||||
title:
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='deployContract.title.parameters'
|
||||
defaultMessage='contract parameters' />
|
||||
)
|
||||
},
|
||||
DEPLOYMENT: {
|
||||
waiting: true,
|
||||
title:
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='deployContract.title.deployment'
|
||||
defaultMessage='deployment' />
|
||||
)
|
||||
},
|
||||
COMPLETED: {
|
||||
title:
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='deployContract.title.completed'
|
||||
defaultMessage='completed' />
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
@ -495,48 +499,53 @@ class DeployContract extends Component {
|
||||
case 'estimateGas':
|
||||
case 'postTransaction':
|
||||
this.setState({
|
||||
deployState:
|
||||
deployState: (
|
||||
<FormattedMessage
|
||||
id='deployContract.state.preparing'
|
||||
defaultMessage='Preparing transaction for network transmission' />
|
||||
)
|
||||
});
|
||||
return;
|
||||
|
||||
case 'checkRequest':
|
||||
this.setState({
|
||||
deployState:
|
||||
deployState: (
|
||||
<FormattedMessage
|
||||
id='deployContract.state.waitSigner'
|
||||
defaultMessage='Waiting for confirmation of the transaction in the Parity Secure Signer' />
|
||||
)
|
||||
});
|
||||
return;
|
||||
|
||||
case 'getTransactionReceipt':
|
||||
this.setState({
|
||||
txhash: data.txhash,
|
||||
deployState:
|
||||
deployState: (
|
||||
<FormattedMessage
|
||||
id='deployContract.state.waitReceipt'
|
||||
defaultMessage='Waiting for the contract deployment transaction receipt' />
|
||||
)
|
||||
});
|
||||
return;
|
||||
|
||||
case 'hasReceipt':
|
||||
case 'getCode':
|
||||
this.setState({
|
||||
deployState:
|
||||
deployState: (
|
||||
<FormattedMessage
|
||||
id='deployContract.state.validatingCode'
|
||||
defaultMessage='Validating the deployed contract code' />
|
||||
)
|
||||
});
|
||||
return;
|
||||
|
||||
case 'completed':
|
||||
this.setState({
|
||||
deployState:
|
||||
deployState: (
|
||||
<FormattedMessage
|
||||
id='deployContract.state.completed'
|
||||
defaultMessage='The contract deployment has been completed' />
|
||||
)
|
||||
});
|
||||
return;
|
||||
|
||||
@ -552,13 +561,19 @@ class DeployContract extends Component {
|
||||
}
|
||||
|
||||
function mapStateToProps (initState, initProps) {
|
||||
const fromAddresses = Object.keys(initProps.accounts);
|
||||
const { accounts } = initProps;
|
||||
|
||||
// Skip Wallet accounts : they can't create Contracts
|
||||
const _accounts = omitBy(accounts, (a) => a.wallet);
|
||||
|
||||
const fromAddresses = Object.keys(_accounts);
|
||||
|
||||
return (state) => {
|
||||
const balances = pick(state.balances.balances, fromAddresses);
|
||||
const { gasLimit } = state.nodeStatus;
|
||||
|
||||
return {
|
||||
accounts: _accounts,
|
||||
balances,
|
||||
gasLimit
|
||||
};
|
||||
|
@ -17,20 +17,24 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { newError } from '~/redux/actions';
|
||||
import { Button, Form, Input, InputChip, Modal } from '~/ui';
|
||||
import { CancelIcon, SaveIcon } from '~/ui/Icons';
|
||||
|
||||
import Store from './store';
|
||||
|
||||
@observer
|
||||
export default class EditMeta extends Component {
|
||||
class EditMeta extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
account: PropTypes.object.isRequired,
|
||||
newError: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
@ -85,7 +89,7 @@ export default class EditMeta extends Component {
|
||||
defaultMessage='(optional) tags' />
|
||||
}
|
||||
onTokensChange={ this.store.setTags }
|
||||
tokens={ tags } />
|
||||
tokens={ tags.slice() } />
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
@ -138,6 +142,20 @@ export default class EditMeta extends Component {
|
||||
|
||||
return this.store
|
||||
.save()
|
||||
.then(() => this.props.onClose());
|
||||
.then(() => this.props.onClose())
|
||||
.catch((error) => {
|
||||
this.props.newError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return bindActionCreators({
|
||||
newError
|
||||
}, dispatch);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(EditMeta);
|
||||
|
@ -20,30 +20,27 @@ import sinon from 'sinon';
|
||||
|
||||
import EditMeta from './';
|
||||
|
||||
import { ACCOUNT } from './editMeta.test.js';
|
||||
import { ACCOUNT, createApi, createRedux } from './editMeta.test.js';
|
||||
|
||||
let api;
|
||||
let component;
|
||||
let instance;
|
||||
let onClose;
|
||||
let reduxStore;
|
||||
|
||||
function render (props) {
|
||||
api = createApi();
|
||||
onClose = sinon.stub();
|
||||
reduxStore = createRedux();
|
||||
|
||||
component = shallow(
|
||||
<EditMeta
|
||||
{ ...props }
|
||||
account={ ACCOUNT }
|
||||
onClose={ onClose } />,
|
||||
{
|
||||
context: {
|
||||
api: {
|
||||
parity: {
|
||||
setAccountName: sinon.stub().resolves(),
|
||||
setAccountMeta: sinon.stub().resolves()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
{ context: { store: reduxStore } }
|
||||
).find('EditMeta').shallow({ context: { api } });
|
||||
instance = component.instance();
|
||||
|
||||
return component;
|
||||
}
|
||||
@ -61,15 +58,29 @@ describe('modals/EditMeta', () => {
|
||||
});
|
||||
|
||||
describe('onSave', () => {
|
||||
it('calls store.save() & props.onClose', () => {
|
||||
const instance = component.instance();
|
||||
it('calls store.save', () => {
|
||||
sinon.spy(instance.store, 'save');
|
||||
|
||||
instance.onSave().then(() => {
|
||||
return instance.onSave().then(() => {
|
||||
expect(instance.store.save).to.have.been.called;
|
||||
instance.store.save.restore();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes the dialog on success', () => {
|
||||
return instance.onSave().then(() => {
|
||||
expect(onClose).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
it('adds newError on failure', () => {
|
||||
sinon.stub(instance.store, 'save').rejects('test');
|
||||
|
||||
return instance.onSave().then(() => {
|
||||
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
|
||||
instance.store.save.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -14,6 +14,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
const ACCOUNT = {
|
||||
address: '0x123456789a123456789a123456789a123456789a',
|
||||
meta: {
|
||||
@ -39,7 +41,28 @@ const ADDRESS = {
|
||||
name: 'Random address'
|
||||
};
|
||||
|
||||
function createApi () {
|
||||
return {
|
||||
parity: {
|
||||
setAccountName: sinon.stub().resolves(),
|
||||
setAccountMeta: sinon.stub().resolves()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createRedux () {
|
||||
return {
|
||||
dispatch: sinon.stub(),
|
||||
subscribe: sinon.stub(),
|
||||
getState: () => {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
ACCOUNT,
|
||||
ADDRESS
|
||||
ADDRESS,
|
||||
createApi,
|
||||
createRedux
|
||||
};
|
||||
|
@ -14,34 +14,35 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { action, computed, observable, toJS, transaction } from 'mobx';
|
||||
import { action, computed, observable, transaction } from 'mobx';
|
||||
|
||||
import { newError } from '~/redux/actions';
|
||||
import { validateName } from '~/util/validation';
|
||||
|
||||
export default class Store {
|
||||
@observable address = null;
|
||||
@observable isAccount = false;
|
||||
@observable description = null;
|
||||
@observable meta = {};
|
||||
@observable meta = null;
|
||||
@observable name = null;
|
||||
@observable nameError = null;
|
||||
@observable passwordHint = null;
|
||||
@observable tags = [];
|
||||
@observable tags = null;
|
||||
|
||||
constructor (api, account) {
|
||||
const { address, name, meta, uuid } = account;
|
||||
|
||||
this._api = api;
|
||||
|
||||
transaction(() => {
|
||||
this.isAccount = !!uuid;
|
||||
this.address = address;
|
||||
this.meta = Object.assign({}, meta || {});
|
||||
this.meta = meta || {};
|
||||
this.name = name || '';
|
||||
|
||||
this.description = this.meta.description || '';
|
||||
this.passwordHint = this.meta.passwordHint || '';
|
||||
this.tags = [].concat((meta || {}).tags || []);
|
||||
this.tags = this.meta.tags && this.meta.tags.peek() || [];
|
||||
});
|
||||
}
|
||||
|
||||
@computed get hasError () {
|
||||
@ -70,7 +71,7 @@ export default class Store {
|
||||
}
|
||||
|
||||
@action setTags = (tags) => {
|
||||
this.tags = [].concat(tags);
|
||||
this.tags = tags.slice();
|
||||
}
|
||||
|
||||
save () {
|
||||
@ -86,12 +87,11 @@ export default class Store {
|
||||
return Promise
|
||||
.all([
|
||||
this._api.parity.setAccountName(this.address, this.name),
|
||||
this._api.parity.setAccountMeta(this.address, Object.assign({}, toJS(this.meta), meta))
|
||||
this._api.parity.setAccountMeta(this.address, Object.assign({}, this.meta, meta))
|
||||
])
|
||||
.catch((error) => {
|
||||
console.error('onSave', error);
|
||||
|
||||
newError(error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -14,22 +14,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { toJS } from 'mobx';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Store from './store';
|
||||
import { ACCOUNT, ADDRESS } from './editMeta.test.js';
|
||||
import { ACCOUNT, ADDRESS, createApi } from './editMeta.test.js';
|
||||
|
||||
let api;
|
||||
let store;
|
||||
|
||||
function createStore (account) {
|
||||
api = {
|
||||
parity: {
|
||||
setAccountName: sinon.stub().resolves(),
|
||||
setAccountMeta: sinon.stub().resolves()
|
||||
}
|
||||
};
|
||||
api = createApi();
|
||||
|
||||
store = new Store(api, account);
|
||||
|
||||
@ -56,12 +48,12 @@ describe('modals/EditMeta/Store', () => {
|
||||
});
|
||||
|
||||
it('extracts the tags', () => {
|
||||
expect(store.tags.peek()).to.deep.equal(ACCOUNT.meta.tags);
|
||||
expect(store.tags).to.deep.equal(ACCOUNT.meta.tags);
|
||||
});
|
||||
|
||||
describe('meta', () => {
|
||||
it('extracts the full meta', () => {
|
||||
expect(toJS(store.meta)).to.deep.equal(ACCOUNT.meta);
|
||||
expect(store.meta).to.deep.equal(ACCOUNT.meta);
|
||||
});
|
||||
|
||||
it('extracts the description', () => {
|
||||
|
@ -22,7 +22,7 @@ import { ContextProvider, muiTheme } from '~/ui';
|
||||
|
||||
import DetailsStep from './';
|
||||
|
||||
import { STORE, CONTRACT } from '../executeContract.test.js';
|
||||
import { createApi, STORE, CONTRACT } from '../executeContract.test.js';
|
||||
|
||||
let component;
|
||||
let onAmountChange;
|
||||
@ -41,7 +41,7 @@ function render (props) {
|
||||
onValueChange = sinon.stub();
|
||||
|
||||
component = mount(
|
||||
<ContextProvider api={ {} } muiTheme={ muiTheme } store={ STORE }>
|
||||
<ContextProvider api={ createApi() } muiTheme={ muiTheme } store={ STORE }>
|
||||
<DetailsStep
|
||||
{ ...props }
|
||||
contract={ CONTRACT }
|
||||
|
@ -39,26 +39,31 @@ const STEP_BUSY_OR_ADVANCED = 1;
|
||||
const STEP_BUSY = 2;
|
||||
|
||||
const TITLES = {
|
||||
transfer:
|
||||
transfer: (
|
||||
<FormattedMessage
|
||||
id='executeContract.steps.transfer'
|
||||
defaultMessage='function details' />,
|
||||
sending:
|
||||
defaultMessage='function details' />
|
||||
),
|
||||
sending: (
|
||||
<FormattedMessage
|
||||
id='executeContract.steps.sending'
|
||||
defaultMessage='sending' />,
|
||||
complete:
|
||||
defaultMessage='sending' />
|
||||
),
|
||||
complete: (
|
||||
<FormattedMessage
|
||||
id='executeContract.steps.complete'
|
||||
defaultMessage='complete' />,
|
||||
advanced:
|
||||
defaultMessage='complete' />
|
||||
),
|
||||
advanced: (
|
||||
<FormattedMessage
|
||||
id='executeContract.steps.advanced'
|
||||
defaultMessage='advanced options' />,
|
||||
rejected:
|
||||
defaultMessage='advanced options' />
|
||||
),
|
||||
rejected: (
|
||||
<FormattedMessage
|
||||
id='executeContract.steps.rejected'
|
||||
defaultMessage='rejected' />
|
||||
)
|
||||
};
|
||||
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
|
||||
const STAGES_ADVANCED = [TITLES.transfer, TITLES.advanced, TITLES.sending, TITLES.complete];
|
||||
@ -384,6 +389,7 @@ class ExecuteContract extends Component {
|
||||
const { advancedOptions, amount, func, minBlock, values } = this.state;
|
||||
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
|
||||
const finalstep = steps.length - 1;
|
||||
|
||||
const options = {
|
||||
gas: this.gasStore.gas,
|
||||
gasPrice: this.gasStore.price,
|
||||
@ -398,10 +404,11 @@ class ExecuteContract extends Component {
|
||||
.postTransaction(options, values)
|
||||
.then((requestId) => {
|
||||
this.setState({
|
||||
busyState:
|
||||
busyState: (
|
||||
<FormattedMessage
|
||||
id='executeContract.busy.waitAuth'
|
||||
defaultMessage='Waiting for authorization in the Parity Signer' />
|
||||
)
|
||||
});
|
||||
|
||||
return api
|
||||
@ -420,10 +427,11 @@ class ExecuteContract extends Component {
|
||||
sending: false,
|
||||
step: finalstep,
|
||||
txhash,
|
||||
busyState:
|
||||
busyState: (
|
||||
<FormattedMessage
|
||||
id='executeContract.busy.posted'
|
||||
defaultMessage='Your transaction has been posted to the network' />
|
||||
)
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -20,7 +20,7 @@ import sinon from 'sinon';
|
||||
|
||||
import ExecuteContract from './';
|
||||
|
||||
import { CONTRACT, STORE } from './executeContract.test.js';
|
||||
import { createApi, CONTRACT, STORE } from './executeContract.test.js';
|
||||
|
||||
let component;
|
||||
let onClose;
|
||||
@ -36,7 +36,7 @@ function render (props) {
|
||||
contract={ CONTRACT }
|
||||
onClose={ onClose }
|
||||
onFromAddressChange={ onFromAddressChange } />,
|
||||
{ context: { api: {}, store: STORE } }
|
||||
{ context: { api: createApi(), store: STORE } }
|
||||
).find('ExecuteContract').shallow();
|
||||
|
||||
return component;
|
||||
|
@ -64,7 +64,19 @@ const STORE = {
|
||||
}
|
||||
};
|
||||
|
||||
function createApi (result = true) {
|
||||
return {
|
||||
parity: {
|
||||
registryAddress: sinon.stub().resolves('0x0000000000000000000000000000000000000000')
|
||||
},
|
||||
util: {
|
||||
sha3: sinon.stub().resolves('0x0000000000000000000000000000000000000000')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
createApi,
|
||||
CONTRACT,
|
||||
STORE
|
||||
};
|
||||
|
@ -14,26 +14,36 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
||||
import CheckIcon from 'material-ui/svg-icons/navigation/check';
|
||||
import SendIcon from 'material-ui/svg-icons/content/send';
|
||||
|
||||
import { Tabs, Tab } from 'material-ui/Tabs';
|
||||
import Paper from 'material-ui/Paper';
|
||||
|
||||
import { Tabs, Tab } from 'material-ui/Tabs';
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { showSnackbar } from '~/redux/providers/snackbarActions';
|
||||
|
||||
import Form, { Input } from '~/ui/Form';
|
||||
import { newError, openSnackbar } from '~/redux/actions';
|
||||
import { Button, Modal, IdentityName, IdentityIcon } from '~/ui';
|
||||
import Form, { Input } from '~/ui/Form';
|
||||
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
|
||||
|
||||
import Store, { CHANGE_ACTION, TEST_ACTION } from './store';
|
||||
import styles from './passwordManager.css';
|
||||
|
||||
const TEST_ACTION = 'TEST_ACTION';
|
||||
const CHANGE_ACTION = 'CHANGE_ACTION';
|
||||
const MSG_SUCCESS_STYLE = {
|
||||
backgroundColor: 'rgba(174, 213, 129, 0.75)'
|
||||
};
|
||||
const MSG_FAILURE_STYLE = {
|
||||
backgroundColor: 'rgba(229, 115, 115, 0.75)'
|
||||
};
|
||||
const TABS_INKBAR_STYLE = {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.55)'
|
||||
};
|
||||
const TABS_ITEM_STYLE = {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)'
|
||||
};
|
||||
|
||||
@observer
|
||||
class PasswordManager extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
@ -41,27 +51,22 @@ class PasswordManager extends Component {
|
||||
|
||||
static propTypes = {
|
||||
account: PropTypes.object.isRequired,
|
||||
showSnackbar: PropTypes.func.isRequired,
|
||||
openSnackbar: PropTypes.func.isRequired,
|
||||
newError: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func
|
||||
}
|
||||
|
||||
state = {
|
||||
action: TEST_ACTION,
|
||||
waiting: false,
|
||||
showMessage: false,
|
||||
message: { value: '', success: true },
|
||||
currentPass: '',
|
||||
newPass: '',
|
||||
repeatNewPass: '',
|
||||
repeatValid: true,
|
||||
passwordHint: this.props.account.meta && this.props.account.meta.passwordHint || ''
|
||||
}
|
||||
store = new Store(this.context.api, this.props.account);
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Modal
|
||||
actions={ this.renderDialogActions() }
|
||||
title='Password Manager'
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='passwordChange.title'
|
||||
defaultMessage='Password Manager' />
|
||||
}
|
||||
visible>
|
||||
{ this.renderAccount() }
|
||||
{ this.renderPage() }
|
||||
@ -71,150 +76,168 @@ class PasswordManager extends Component {
|
||||
}
|
||||
|
||||
renderMessage () {
|
||||
const { message, showMessage } = this.state;
|
||||
const { infoMessage } = this.store;
|
||||
|
||||
const style = message.success
|
||||
? {
|
||||
backgroundColor: 'rgba(174, 213, 129, 0.75)'
|
||||
}
|
||||
: {
|
||||
backgroundColor: 'rgba(229, 115, 115, 0.75)'
|
||||
};
|
||||
|
||||
const classes = [ styles.message ];
|
||||
|
||||
if (!showMessage) {
|
||||
classes.push(styles.hideMessage);
|
||||
if (!infoMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper
|
||||
zDepth={ 1 }
|
||||
style={ style }
|
||||
className={ classes.join(' ') }>
|
||||
{ message.value }
|
||||
className={ `${styles.message}` }
|
||||
style={
|
||||
infoMessage.success
|
||||
? MSG_SUCCESS_STYLE
|
||||
: MSG_FAILURE_STYLE
|
||||
}
|
||||
zDepth={ 1 }>
|
||||
{ infoMessage.value }
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
renderAccount () {
|
||||
const { account } = this.props;
|
||||
const { address, meta } = account;
|
||||
|
||||
const passwordHint = meta && meta.passwordHint
|
||||
? (
|
||||
<span className={ styles.passwordHint }>
|
||||
<span className={ styles.hintLabel }>Hint </span>
|
||||
{ meta.passwordHint }
|
||||
</span>
|
||||
)
|
||||
: null;
|
||||
const { address, passwordHint } = this.store;
|
||||
|
||||
return (
|
||||
<div className={ styles.accountContainer }>
|
||||
<IdentityIcon
|
||||
address={ address }
|
||||
/>
|
||||
<IdentityIcon address={ address } />
|
||||
<div className={ styles.accountInfos }>
|
||||
<IdentityName
|
||||
className={ styles.accountName }
|
||||
address={ address }
|
||||
unknown
|
||||
/>
|
||||
className={ styles.accountName }
|
||||
unknown />
|
||||
<span className={ styles.accountAddress }>
|
||||
{ address }
|
||||
</span>
|
||||
{ passwordHint }
|
||||
<span className={ styles.passwordHint }>
|
||||
<span className={ styles.hintLabel }>Hint </span>
|
||||
{ passwordHint || '-' }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPage () {
|
||||
const { account } = this.props;
|
||||
const { waiting, repeatValid } = this.state;
|
||||
const disabled = !!waiting;
|
||||
|
||||
const repeatError = repeatValid
|
||||
? null
|
||||
: 'the two passwords differ';
|
||||
|
||||
const { meta } = account;
|
||||
const passwordHint = meta && meta.passwordHint || '';
|
||||
const { busy, isRepeatValid, passwordHint } = this.store;
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
inkBarStyle={ {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.55)'
|
||||
} }
|
||||
tabItemContainerStyle={ {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)'
|
||||
} }
|
||||
>
|
||||
inkBarStyle={ TABS_INKBAR_STYLE }
|
||||
tabItemContainerStyle={ TABS_ITEM_STYLE }>
|
||||
<Tab
|
||||
onActive={ this.handleTestActive }
|
||||
label='Test Password'
|
||||
>
|
||||
<Form
|
||||
className={ styles.form }
|
||||
>
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.tabTest.label'
|
||||
defaultMessage='Test Password' />
|
||||
}
|
||||
onActive={ this.onActivateTestTab }>
|
||||
<Form className={ styles.form }>
|
||||
<div>
|
||||
<Input
|
||||
label='password'
|
||||
hint='your current password for this account'
|
||||
type='password'
|
||||
disabled={ busy }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='passwordChange.testPassword.hint'
|
||||
defaultMessage='your account password' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.testPassword.label'
|
||||
defaultMessage='password' />
|
||||
}
|
||||
onChange={ this.onEditTestPassword }
|
||||
onSubmit={ this.testPassword }
|
||||
submitOnBlur={ false }
|
||||
disabled={ disabled }
|
||||
onSubmit={ this.handleTestPassword }
|
||||
onChange={ this.onEditCurrent } />
|
||||
type='password' />
|
||||
</div>
|
||||
</Form>
|
||||
</Tab>
|
||||
<Tab
|
||||
onActive={ this.handleChangeActive }
|
||||
label='Change Password'
|
||||
>
|
||||
<Form
|
||||
className={ styles.form }
|
||||
>
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.tabChange.label'
|
||||
defaultMessage='Change Password' />
|
||||
}
|
||||
onActive={ this.onActivateChangeTab }>
|
||||
<Form className={ styles.form }>
|
||||
<div>
|
||||
<Input
|
||||
label='current password'
|
||||
hint='your current password for this account'
|
||||
type='password'
|
||||
disabled={ busy }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='passwordChange.currentPassword.hint'
|
||||
defaultMessage='your current password for this account' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.currentPassword.label'
|
||||
defaultMessage='current password' />
|
||||
}
|
||||
onChange={ this.onEditCurrentPassword }
|
||||
onSubmit={ this.changePassword }
|
||||
submitOnBlur={ false }
|
||||
disabled={ disabled }
|
||||
onSubmit={ this.handleChangePassword }
|
||||
onChange={ this.onEditCurrent } />
|
||||
type='password' />
|
||||
<Input
|
||||
label='(optional) new password hint'
|
||||
hint='hint for the new password'
|
||||
disabled={ busy }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='passwordChange.passwordHint.hint'
|
||||
defaultMessage='hint for the new password' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.passwordHint.label'
|
||||
defaultMessage='(optional) new password hint' />
|
||||
}
|
||||
onChange={ this.onEditNewPasswordHint }
|
||||
onSubmit={ this.changePassword }
|
||||
submitOnBlur={ false }
|
||||
value={ passwordHint }
|
||||
disabled={ disabled }
|
||||
onSubmit={ this.handleChangePassword }
|
||||
onChange={ this.onEditHint } />
|
||||
value={ passwordHint } />
|
||||
<div className={ styles.passwords }>
|
||||
<div className={ styles.password }>
|
||||
<Input
|
||||
label='new password'
|
||||
hint='the new password for this account'
|
||||
type='password'
|
||||
disabled={ busy }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='passwordChange.newPassword.hint'
|
||||
defaultMessage='the new password for this account' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.newPassword.label'
|
||||
defaultMessage='new password' />
|
||||
}
|
||||
onChange={ this.onEditNewPassword }
|
||||
onSubmit={ this.changePassword }
|
||||
submitOnBlur={ false }
|
||||
disabled={ disabled }
|
||||
onSubmit={ this.handleChangePassword }
|
||||
onChange={ this.onEditNew } />
|
||||
type='password' />
|
||||
</div>
|
||||
<div className={ styles.password }>
|
||||
<Input
|
||||
label='repeat new password'
|
||||
hint='repeat the new password for this account'
|
||||
type='password'
|
||||
disabled={ busy }
|
||||
error={
|
||||
isRepeatValid
|
||||
? null
|
||||
: <FormattedMessage
|
||||
id='passwordChange.repeatPassword.error'
|
||||
defaultMessage='the supplied passwords do not match' />
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='passwordChange.repeatPassword.hint'
|
||||
defaultMessage='repeat the new password for this account' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.repeatPassword.label'
|
||||
defaultMessage='repeat new password' />
|
||||
}
|
||||
onChange={ this.onEditNewPasswordRepeat }
|
||||
onSubmit={ this.changePassword }
|
||||
submitOnBlur={ false }
|
||||
error={ repeatError }
|
||||
disabled={ disabled }
|
||||
onSubmit={ this.handleChangePassword }
|
||||
onChange={ this.onEditRepeatNew } />
|
||||
type='password' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -225,172 +248,126 @@ class PasswordManager extends Component {
|
||||
}
|
||||
|
||||
renderDialogActions () {
|
||||
const { actionTab, busy, isRepeatValid } = this.store;
|
||||
const { onClose } = this.props;
|
||||
const { action, waiting, repeatValid } = this.state;
|
||||
|
||||
const cancelBtn = (
|
||||
<Button
|
||||
icon={ <ContentClear /> }
|
||||
label='Cancel'
|
||||
icon={ <CancelIcon /> }
|
||||
key='cancel'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.button.cancel'
|
||||
defaultMessage='Cancel' />
|
||||
}
|
||||
onClick={ onClose } />
|
||||
);
|
||||
|
||||
if (waiting) {
|
||||
const waitingBtn = (
|
||||
if (busy) {
|
||||
return [
|
||||
cancelBtn,
|
||||
<Button
|
||||
disabled
|
||||
label='Wait...' />
|
||||
);
|
||||
|
||||
return [ cancelBtn, waitingBtn ];
|
||||
key='wait'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.button.wait'
|
||||
defaultMessage='Wait...' />
|
||||
} />
|
||||
];
|
||||
}
|
||||
|
||||
if (action === TEST_ACTION) {
|
||||
const testBtn = (
|
||||
if (actionTab === TEST_ACTION) {
|
||||
return [
|
||||
cancelBtn,
|
||||
<Button
|
||||
icon={ <CheckIcon /> }
|
||||
label='Test'
|
||||
onClick={ this.handleTestPassword } />
|
||||
);
|
||||
|
||||
return [ cancelBtn, testBtn ];
|
||||
key='test'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.button.test'
|
||||
defaultMessage='Test' />
|
||||
}
|
||||
onClick={ this.testPassword } />
|
||||
];
|
||||
}
|
||||
|
||||
const changeBtn = (
|
||||
return [
|
||||
cancelBtn,
|
||||
<Button
|
||||
disabled={ !repeatValid }
|
||||
disabled={ !isRepeatValid }
|
||||
icon={ <SendIcon /> }
|
||||
label='Change'
|
||||
onClick={ this.handleChangePassword } />
|
||||
key='change'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='passwordChange.button.change'
|
||||
defaultMessage='Change' />
|
||||
}
|
||||
onClick={ this.changePassword } />
|
||||
];
|
||||
}
|
||||
|
||||
onActivateChangeTab = () => {
|
||||
this.store.setActionTab(CHANGE_ACTION);
|
||||
}
|
||||
|
||||
onActivateTestTab = () => {
|
||||
this.store.setActionTab(TEST_ACTION);
|
||||
}
|
||||
|
||||
onEditCurrentPassword = (event, password) => {
|
||||
this.store.setPassword(password);
|
||||
}
|
||||
|
||||
onEditNewPassword = (event, password) => {
|
||||
this.store.setNewPassword(password);
|
||||
}
|
||||
|
||||
onEditNewPasswordHint = (event, passwordHint) => {
|
||||
this.store.setNewPasswordHint(passwordHint);
|
||||
}
|
||||
|
||||
onEditNewPasswordRepeat = (event, password) => {
|
||||
this.store.setNewPasswordRepeat(password);
|
||||
}
|
||||
|
||||
onEditTestPassword = (event, password) => {
|
||||
this.store.setValidatePassword(password);
|
||||
}
|
||||
|
||||
changePassword = () => {
|
||||
return this.store
|
||||
.changePassword()
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
this.props.openSnackbar(
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='passwordChange.success'
|
||||
defaultMessage='Your password has been successfully changed' />
|
||||
</div>
|
||||
);
|
||||
|
||||
return [ cancelBtn, changeBtn ];
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
onEditCurrent = (event, value) => {
|
||||
this.setState({
|
||||
currentPass: value,
|
||||
showMessage: false
|
||||
});
|
||||
}
|
||||
|
||||
onEditNew = (event, value) => {
|
||||
const repeatValid = value === this.state.repeatNewPass;
|
||||
|
||||
this.setState({
|
||||
newPass: value,
|
||||
showMessage: false,
|
||||
repeatValid
|
||||
});
|
||||
}
|
||||
|
||||
onEditRepeatNew = (event, value) => {
|
||||
const repeatValid = value === this.state.newPass;
|
||||
|
||||
this.setState({
|
||||
repeatNewPass: value,
|
||||
showMessage: false,
|
||||
repeatValid
|
||||
});
|
||||
}
|
||||
|
||||
onEditHint = (event, value) => {
|
||||
this.setState({
|
||||
passwordHint: value,
|
||||
showMessage: false
|
||||
});
|
||||
}
|
||||
|
||||
handleTestActive = () => {
|
||||
this.setState({
|
||||
action: TEST_ACTION,
|
||||
showMessage: false
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeActive = () => {
|
||||
this.setState({
|
||||
action: CHANGE_ACTION,
|
||||
showMessage: false
|
||||
});
|
||||
}
|
||||
|
||||
handleTestPassword = () => {
|
||||
const { account } = this.props;
|
||||
const { currentPass } = this.state;
|
||||
|
||||
this.setState({ waiting: true, showMessage: false });
|
||||
|
||||
this.context
|
||||
.api.parity
|
||||
.testPassword(account.address, currentPass)
|
||||
.then(correct => {
|
||||
const message = correct
|
||||
? { value: 'This password is correct', success: true }
|
||||
: { value: 'This password is not correct', success: false };
|
||||
|
||||
this.setState({ waiting: false, message, showMessage: true });
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('passwordManager::handleTestPassword', e);
|
||||
this.setState({ waiting: false });
|
||||
.catch((error) => {
|
||||
this.props.newError(error);
|
||||
});
|
||||
}
|
||||
|
||||
handleChangePassword = () => {
|
||||
const { account, showSnackbar, onClose } = this.props;
|
||||
const { currentPass, newPass, repeatNewPass, passwordHint } = this.state;
|
||||
|
||||
if (repeatNewPass !== newPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ waiting: true, showMessage: false });
|
||||
|
||||
this.context
|
||||
.api.parity
|
||||
.testPassword(account.address, currentPass)
|
||||
.then(correct => {
|
||||
if (!correct) {
|
||||
const message = {
|
||||
value: 'This provided current password is not correct',
|
||||
success: false
|
||||
};
|
||||
|
||||
this.setState({ waiting: false, message, showMessage: true });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const meta = Object.assign({}, account.meta, {
|
||||
passwordHint
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
this.context
|
||||
.api.parity
|
||||
.setAccountMeta(account.address, meta),
|
||||
|
||||
this.context
|
||||
.api.parity
|
||||
.changePassword(account.address, currentPass, newPass)
|
||||
])
|
||||
.then(() => {
|
||||
showSnackbar(<div>Your password has been successfully changed.</div>);
|
||||
this.setState({ waiting: false, showMessage: false });
|
||||
onClose();
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('passwordManager::handleChangePassword', e);
|
||||
this.setState({ waiting: false });
|
||||
testPassword = () => {
|
||||
return this.store
|
||||
.testPassword()
|
||||
.catch((error) => {
|
||||
this.props.newError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return bindActionCreators({
|
||||
showSnackbar
|
||||
openSnackbar,
|
||||
newError
|
||||
}, dispatch);
|
||||
}
|
||||
|
||||
|
110
js/src/modals/PasswordManager/passwordManager.spec.js
Normal file
110
js/src/modals/PasswordManager/passwordManager.spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
54
js/src/modals/PasswordManager/passwordManager.test.js
Normal file
54
js/src/modals/PasswordManager/passwordManager.test.js
Normal 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
|
||||
};
|
161
js/src/modals/PasswordManager/store.js
Normal file
161
js/src/modals/PasswordManager/store.js
Normal 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
|
||||
};
|
103
js/src/modals/PasswordManager/store.spec.js
Normal file
103
js/src/modals/PasswordManager/store.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -383,9 +383,7 @@ export default class TransferStore {
|
||||
|
||||
const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag);
|
||||
const format = new BigNumber(senderBalance.token.format || 1);
|
||||
const available = isWallet
|
||||
? this.api.util.fromWei(new BigNumber(senderBalance.value))
|
||||
: (new BigNumber(senderBalance.value)).div(format);
|
||||
const available = new BigNumber(senderBalance.value).div(format);
|
||||
|
||||
let { value, valueError } = this;
|
||||
let totalEth = gasTotal;
|
||||
@ -428,7 +426,6 @@ export default class TransferStore {
|
||||
|
||||
send () {
|
||||
const { options, values } = this._getTransferParams();
|
||||
|
||||
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
|
||||
|
||||
return this._getTransferMethod().postTransaction(options, values);
|
||||
@ -440,18 +437,9 @@ export default class TransferStore {
|
||||
}
|
||||
|
||||
estimateGas () {
|
||||
if (this.isEth || !this.isWallet) {
|
||||
return this._estimateGas();
|
||||
}
|
||||
|
||||
return Promise
|
||||
.all([
|
||||
this._estimateGas(true),
|
||||
this._estimateGas()
|
||||
])
|
||||
.then((results) => results[0].plus(results[1]));
|
||||
}
|
||||
|
||||
_getTransferMethod (gas = false, forceToken = false) {
|
||||
const { isEth, isWallet } = this;
|
||||
|
||||
|
@ -36,7 +36,7 @@ class WalletSettings extends Component {
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
accounts: PropTypes.object.isRequired,
|
||||
accountsInfo: PropTypes.object.isRequired,
|
||||
wallet: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
senders: PropTypes.object.isRequired
|
||||
@ -113,7 +113,7 @@ class WalletSettings extends Component {
|
||||
default:
|
||||
case 'EDIT':
|
||||
const { wallet, errors } = this.store;
|
||||
const { accounts, senders } = this.props;
|
||||
const { accountsInfo, senders } = this.props;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
@ -137,7 +137,7 @@ class WalletSettings extends Component {
|
||||
label='other wallet owners'
|
||||
value={ wallet.owners.slice() }
|
||||
onChange={ this.store.onOwnersChange }
|
||||
accounts={ accounts }
|
||||
accounts={ accountsInfo }
|
||||
param='address[]'
|
||||
/>
|
||||
|
||||
@ -190,7 +190,7 @@ class WalletSettings extends Component {
|
||||
}
|
||||
|
||||
renderChange (change) {
|
||||
const { accounts } = this.props;
|
||||
const { accountsInfo } = this.props;
|
||||
|
||||
switch (change.type) {
|
||||
case 'dailylimit':
|
||||
@ -229,7 +229,7 @@ class WalletSettings extends Component {
|
||||
<InputAddress
|
||||
disabled
|
||||
value={ change.value }
|
||||
accounts={ accounts }
|
||||
accounts={ accountsInfo }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -243,7 +243,7 @@ class WalletSettings extends Component {
|
||||
<InputAddress
|
||||
disabled
|
||||
value={ change.value }
|
||||
accounts={ accounts }
|
||||
accounts={ accountsInfo }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -329,7 +329,7 @@ function mapStateToProps (initState, initProps) {
|
||||
const senders = pick(accounts, owners);
|
||||
|
||||
return () => {
|
||||
return { accounts: accountsInfo, senders };
|
||||
return { accountsInfo, senders };
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,8 @@ const STEPS = {
|
||||
};
|
||||
|
||||
export default class WalletSettingsStore {
|
||||
accounts = {};
|
||||
|
||||
@observable step = null;
|
||||
@observable requests = [];
|
||||
@observable deployState = '';
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import { newError } from '~/ui/Errors/actions';
|
||||
import { setAddressImage } from './providers/imagesActions';
|
||||
import { openSnackbar, showSnackbar } from './providers/snackbarActions';
|
||||
import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions';
|
||||
import { toggleView } from '~/views/Settings/actions';
|
||||
|
||||
@ -23,6 +24,8 @@ export {
|
||||
newError,
|
||||
clearStatusLogs,
|
||||
setAddressImage,
|
||||
openSnackbar,
|
||||
showSnackbar,
|
||||
toggleStatusLogs,
|
||||
toggleStatusRefresh,
|
||||
toggleView
|
||||
|
@ -22,6 +22,7 @@ import SignerMiddleware from './providers/signerMiddleware';
|
||||
|
||||
import statusMiddleware from '~/views/Status/middleware';
|
||||
import CertificationsMiddleware from './providers/certifications/middleware';
|
||||
import ChainMiddleware from './providers/chainMiddleware';
|
||||
|
||||
export default function (api, browserHistory) {
|
||||
const errors = new ErrorsMiddleware();
|
||||
@ -30,12 +31,14 @@ export default function (api, browserHistory) {
|
||||
const status = statusMiddleware();
|
||||
const certifications = new CertificationsMiddleware();
|
||||
const routeMiddleware = routerMiddleware(browserHistory);
|
||||
const chain = new ChainMiddleware();
|
||||
|
||||
const middleware = [
|
||||
settings.toMiddleware(),
|
||||
signer.toMiddleware(),
|
||||
errors.toMiddleware(),
|
||||
certifications.toMiddleware()
|
||||
certifications.toMiddleware(),
|
||||
chain.toMiddleware()
|
||||
];
|
||||
|
||||
return middleware.concat(status, routeMiddleware, thunk);
|
||||
|
@ -173,17 +173,18 @@ export function fetchTokens (_tokenIds) {
|
||||
export function fetchBalances (_addresses) {
|
||||
return (dispatch, getState) => {
|
||||
const { api, personal } = getState();
|
||||
const { visibleAccounts, accounts } = personal;
|
||||
const { visibleAccounts, accountsInfo } = personal;
|
||||
|
||||
const addresses = uniq(_addresses || visibleAccounts || []);
|
||||
const addresses = uniq((_addresses || visibleAccounts || []).concat(Object.keys(accountsInfo)));
|
||||
|
||||
if (addresses.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// With only a single account, more info will be displayed.
|
||||
const fullFetch = addresses.length === 1;
|
||||
|
||||
const addressesToFetch = uniq(addresses.concat(Object.keys(accounts)));
|
||||
const addressesToFetch = uniq(addresses);
|
||||
|
||||
return Promise
|
||||
.all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch)))
|
||||
|
38
js/src/redux/providers/chainMiddleware.js
Normal file
38
js/src/redux/providers/chainMiddleware.js
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
@ -14,14 +14,18 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, intersection } from 'lodash';
|
||||
|
||||
import { fetchBalances } from './balancesActions';
|
||||
import { attachWallets } from './walletActions';
|
||||
|
||||
import Contract from '~/api/contract';
|
||||
import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore';
|
||||
import WalletsUtils from '~/util/wallets';
|
||||
import { wallet as WalletAbi } from '~/contracts/abi';
|
||||
|
||||
export function personalAccountsInfo (accountsInfo) {
|
||||
const addresses = [];
|
||||
const accounts = {};
|
||||
const contacts = {};
|
||||
const contracts = {};
|
||||
@ -32,6 +36,7 @@ export function personalAccountsInfo (accountsInfo) {
|
||||
.filter((account) => account.uuid || !account.meta.deleted)
|
||||
.forEach((account) => {
|
||||
if (account.uuid) {
|
||||
addresses.push(account.address);
|
||||
accounts[account.address] = account;
|
||||
} else if (account.meta.wallet) {
|
||||
account.wallet = true;
|
||||
@ -46,14 +51,52 @@ export function personalAccountsInfo (accountsInfo) {
|
||||
// Load user contracts for Method Decoding
|
||||
MethodDecodingStore.loadContracts(contracts);
|
||||
|
||||
return (dispatch) => {
|
||||
return (dispatch, getState) => {
|
||||
const { api } = getState();
|
||||
|
||||
const _fetchOwners = Object
|
||||
.values(wallets)
|
||||
.map((wallet) => {
|
||||
const walletContract = new Contract(api, WalletAbi);
|
||||
return WalletsUtils.fetchOwners(walletContract.at(wallet.address));
|
||||
});
|
||||
|
||||
Promise
|
||||
.all(_fetchOwners)
|
||||
.then((walletsOwners) => {
|
||||
return Object
|
||||
.values(wallets)
|
||||
.map((wallet, index) => {
|
||||
wallet.owners = walletsOwners[index].map((owner) => ({
|
||||
address: owner,
|
||||
name: accountsInfo[owner] && accountsInfo[owner].name || owner
|
||||
}));
|
||||
|
||||
return wallet;
|
||||
});
|
||||
})
|
||||
.then((_wallets) => {
|
||||
_wallets.forEach((wallet) => {
|
||||
const owners = wallet.owners.map((o) => o.address);
|
||||
|
||||
// Owners ∩ Addresses not null : Wallet is owned
|
||||
// by one of the accounts
|
||||
if (intersection(owners, addresses).length > 0) {
|
||||
accounts[wallet.address] = wallet;
|
||||
} else {
|
||||
contacts[wallet.address] = wallet;
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
accountsInfo,
|
||||
accounts, contacts, contracts, wallets
|
||||
accounts, contacts, contracts
|
||||
};
|
||||
|
||||
dispatch(_personalAccountsInfo(data));
|
||||
dispatch(attachWallets(wallets));
|
||||
dispatch(fetchBalances());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -25,14 +25,13 @@ const initialState = {
|
||||
hasContacts: false,
|
||||
contracts: {},
|
||||
hasContracts: false,
|
||||
wallet: {},
|
||||
hasWallets: false,
|
||||
visibleAccounts: []
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
personalAccountsInfo (state, action) {
|
||||
const { accountsInfo, accounts, contacts, contracts, wallets } = action;
|
||||
const accountsInfo = action.accountsInfo || state.accountsInfo;
|
||||
const { accounts, contacts, contracts } = action;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
accountsInfo,
|
||||
@ -41,9 +40,7 @@ export default handleActions({
|
||||
contacts,
|
||||
hasContacts: Object.keys(contacts).length !== 0,
|
||||
contracts,
|
||||
hasContracts: Object.keys(contracts).length !== 0,
|
||||
wallets,
|
||||
hasWallets: Object.keys(wallets).length !== 0
|
||||
hasContracts: Object.keys(contracts).length !== 0
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -90,7 +90,7 @@ export default handleActions({
|
||||
signerSuccessRejectRequest (state, action) {
|
||||
const { id } = action.payload;
|
||||
const rejected = Object.assign(
|
||||
state.pending.find(p => p.id === id),
|
||||
state.pending.find(p => p.id === id) || { id },
|
||||
{ status: 'rejected' }
|
||||
);
|
||||
return {
|
||||
|
@ -20,7 +20,7 @@ export function showSnackbar (message, cooldown) {
|
||||
};
|
||||
}
|
||||
|
||||
function openSnackbar (message, cooldown) {
|
||||
export function openSnackbar (message, cooldown) {
|
||||
return {
|
||||
type: 'openSnackbar',
|
||||
message, cooldown
|
||||
|
@ -52,6 +52,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.75em;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
flex: 1;
|
||||
|
||||
|
@ -43,7 +43,7 @@ export default class AccountCard extends Component {
|
||||
const { account } = this.props;
|
||||
const { copied } = this.state;
|
||||
|
||||
const { address, name, meta = {} } = account;
|
||||
const { address, name, description, meta = {} } = account;
|
||||
|
||||
const displayName = (name && name.toUpperCase()) || address;
|
||||
const { tags = [] } = meta;
|
||||
@ -70,6 +70,7 @@ export default class AccountCard extends Component {
|
||||
</div>
|
||||
|
||||
{ this.renderTags(tags, address) }
|
||||
{ this.renderDescription(description) }
|
||||
{ this.renderAddress(displayName, address) }
|
||||
{ this.renderBalance(address) }
|
||||
</div>
|
||||
@ -77,6 +78,18 @@ export default class AccountCard extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderDescription (description) {
|
||||
if (!description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.description }>
|
||||
<span>{ description }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAddress (name, address) {
|
||||
if (name === address) {
|
||||
return null;
|
||||
|
@ -30,15 +30,22 @@ export default class Container extends Component {
|
||||
compact: PropTypes.bool,
|
||||
light: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
tabIndex: PropTypes.number,
|
||||
title: nodeOrStringProptype()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, className, compact, light, style } = this.props;
|
||||
const { children, className, compact, light, style, tabIndex } = this.props;
|
||||
const classes = `${styles.container} ${light ? styles.light : ''} ${className}`;
|
||||
|
||||
const props = {};
|
||||
|
||||
if (Number.isInteger(tabIndex)) {
|
||||
props.tabIndex = tabIndex;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ classes } style={ style }>
|
||||
<div className={ classes } style={ style } { ...props }>
|
||||
<Card className={ compact ? styles.compact : styles.padded }>
|
||||
{ this.renderTitle() }
|
||||
{ children }
|
||||
|
@ -19,14 +19,17 @@ import ReactDOM from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import keycode, { codes } from 'keycode';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline';
|
||||
|
||||
import AccountCard from '~/ui/AccountCard';
|
||||
import InputAddress from '~/ui/Form/InputAddress';
|
||||
import Portal from '~/ui/Portal';
|
||||
import { nodeOrStringProptype } from '~/util/proptypes';
|
||||
import { validateAddress } from '~/util/validation';
|
||||
|
||||
import AddressSelectStore from './addressSelectStore';
|
||||
import styles from './addressSelect.css';
|
||||
|
||||
const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' };
|
||||
@ -34,8 +37,11 @@ const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' };
|
||||
// Current Form ID
|
||||
let currentId = 1;
|
||||
|
||||
@observer
|
||||
class AddressSelect extends Component {
|
||||
static contextTypes = {
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
api: PropTypes.object.isRequired,
|
||||
muiTheme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@ -50,29 +56,31 @@ class AddressSelect extends Component {
|
||||
contacts: PropTypes.object,
|
||||
contracts: PropTypes.object,
|
||||
tokens: PropTypes.object,
|
||||
wallets: PropTypes.object,
|
||||
|
||||
// Optional props
|
||||
allowCopy: PropTypes.bool,
|
||||
allowInput: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
hint: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.string
|
||||
error: nodeOrStringProptype(),
|
||||
hint: nodeOrStringProptype(),
|
||||
label: nodeOrStringProptype(),
|
||||
readOnly: PropTypes.bool,
|
||||
value: nodeOrStringProptype()
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: ''
|
||||
};
|
||||
|
||||
store = new AddressSelectStore(this.context.api);
|
||||
|
||||
state = {
|
||||
expanded: false,
|
||||
focused: false,
|
||||
focusedCat: null,
|
||||
focusedItem: null,
|
||||
inputFocused: false,
|
||||
inputValue: '',
|
||||
values: []
|
||||
inputValue: ''
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
@ -80,7 +88,7 @@ class AddressSelect extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.values && this.values.length > 0) {
|
||||
if (this.store.values && this.store.values.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -88,36 +96,7 @@ class AddressSelect extends Component {
|
||||
}
|
||||
|
||||
setValues (props = this.props) {
|
||||
const { accounts = {}, contracts = {}, contacts = {}, wallets = {} } = props;
|
||||
|
||||
const accountsN = Object.keys(accounts).length;
|
||||
const contractsN = Object.keys(contracts).length;
|
||||
const contactsN = Object.keys(contacts).length;
|
||||
const walletsN = Object.keys(wallets).length;
|
||||
|
||||
if (accountsN + contractsN + contactsN + walletsN === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.values = [
|
||||
{
|
||||
label: 'accounts',
|
||||
values: [].concat(
|
||||
Object.values(wallets),
|
||||
Object.values(accounts)
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'contacts',
|
||||
values: Object.values(contacts)
|
||||
},
|
||||
{
|
||||
label: 'contracts',
|
||||
values: Object.values(contracts)
|
||||
}
|
||||
].filter((cat) => cat.values.length > 0);
|
||||
|
||||
this.handleChange();
|
||||
this.store.setValues(props);
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -144,12 +123,12 @@ class AddressSelect extends Component {
|
||||
|
||||
renderInput () {
|
||||
const { focused } = this.state;
|
||||
const { accountsInfo, disabled, error, hint, label, value } = this.props;
|
||||
const { accountsInfo, allowCopy, disabled, error, hint, label, readOnly, value } = this.props;
|
||||
|
||||
const input = (
|
||||
<InputAddress
|
||||
accountsInfo={ accountsInfo }
|
||||
allowCopy={ false }
|
||||
allowCopy={ allowCopy }
|
||||
disabled={ disabled }
|
||||
error={ error }
|
||||
hint={ hint }
|
||||
@ -162,7 +141,7 @@ class AddressSelect extends Component {
|
||||
/>
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
if (disabled || readOnly) {
|
||||
return input;
|
||||
}
|
||||
|
||||
@ -175,14 +154,20 @@ class AddressSelect extends Component {
|
||||
|
||||
renderContent () {
|
||||
const { muiTheme } = this.context;
|
||||
const { hint, disabled, label } = this.props;
|
||||
const { hint, disabled, label, readOnly } = this.props;
|
||||
const { expanded, inputFocused } = this.state;
|
||||
|
||||
if (disabled) {
|
||||
if (disabled || readOnly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = `addressSelect_${++currentId}`;
|
||||
const ilHint = typeof hint === 'string' || !(hint && hint.props)
|
||||
? (hint || '')
|
||||
: this.context.intl.formatMessage(
|
||||
hint.props,
|
||||
hint.props.values || {}
|
||||
);
|
||||
|
||||
return (
|
||||
<Portal
|
||||
@ -197,7 +182,7 @@ class AddressSelect extends Component {
|
||||
<input
|
||||
id={ id }
|
||||
className={ styles.input }
|
||||
placeholder={ hint }
|
||||
placeholder={ ilHint }
|
||||
|
||||
onBlur={ this.handleInputBlur }
|
||||
onFocus={ this.handleInputFocus }
|
||||
@ -216,6 +201,7 @@ class AddressSelect extends Component {
|
||||
</div>
|
||||
|
||||
{ this.renderCurrentInput() }
|
||||
{ this.renderRegistryValues() }
|
||||
{ this.renderAccounts() }
|
||||
</Portal>
|
||||
);
|
||||
@ -241,8 +227,28 @@ class AddressSelect extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderRegistryValues () {
|
||||
const { registryValues } = this.store;
|
||||
|
||||
if (registryValues.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accounts = registryValues
|
||||
.map((registryValue, index) => {
|
||||
const account = { ...registryValue, index: `${registryValue.address}_${index}` };
|
||||
return this.renderAccountCard(account);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ accounts }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAccounts () {
|
||||
const { values } = this.state;
|
||||
const { values } = this.store;
|
||||
|
||||
if (values.length === 0) {
|
||||
return (
|
||||
@ -257,8 +263,8 @@ class AddressSelect extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
const categories = values.map((category) => {
|
||||
return this.renderCategory(category.label, category.values);
|
||||
const categories = values.map((category, index) => {
|
||||
return this.renderCategory(category, index);
|
||||
});
|
||||
|
||||
return (
|
||||
@ -268,7 +274,8 @@ class AddressSelect extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderCategory (name, values = []) {
|
||||
renderCategory (category, index) {
|
||||
const { label, key, values = [] } = category;
|
||||
let content;
|
||||
|
||||
if (values.length === 0) {
|
||||
@ -292,8 +299,8 @@ class AddressSelect extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.category } key={ name }>
|
||||
<div className={ styles.title }>{ name }</div>
|
||||
<div className={ styles.category } key={ `${key}_${index}` }>
|
||||
<div className={ styles.title }>{ label }</div>
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
@ -306,7 +313,7 @@ class AddressSelect extends Component {
|
||||
const balance = balances[address];
|
||||
const account = {
|
||||
...accountsInfo[address],
|
||||
address, index
|
||||
..._account
|
||||
};
|
||||
|
||||
return (
|
||||
@ -325,9 +332,10 @@ class AddressSelect extends Component {
|
||||
this.inputRef = refId;
|
||||
}
|
||||
|
||||
handleCustomInput = () => {
|
||||
validateCustomInput = () => {
|
||||
const { allowInput } = this.props;
|
||||
const { inputValue, values } = this.state;
|
||||
const { inputValue } = this.store;
|
||||
const { values } = this.store;
|
||||
|
||||
// If input is HEX and allowInput === true, send it
|
||||
if (allowInput && inputValue && /^(0x)?([0-9a-f])+$/i.test(inputValue)) {
|
||||
@ -335,8 +343,8 @@ class AddressSelect extends Component {
|
||||
}
|
||||
|
||||
// If only one value, select it
|
||||
if (values.length === 1 && values[0].values.length === 1) {
|
||||
const value = values[0].values[0];
|
||||
if (values.reduce((cur, cat) => cur + cat.values.length, 0) === 1) {
|
||||
const value = values.find((cat) => cat.values.length > 0).values[0];
|
||||
return this.handleClick(value.address);
|
||||
}
|
||||
}
|
||||
@ -361,7 +369,7 @@ class AddressSelect extends Component {
|
||||
case 'enter':
|
||||
const index = this.state.focusedItem;
|
||||
if (!index) {
|
||||
return this.handleCustomInput();
|
||||
return this.validateCustomInput();
|
||||
}
|
||||
|
||||
return this.handleDOMAction(`account_${index}`, 'click');
|
||||
@ -408,10 +416,11 @@ class AddressSelect extends Component {
|
||||
}
|
||||
|
||||
handleNavigation = (direction, event) => {
|
||||
const { focusedItem, focusedCat, values } = this.state;
|
||||
const { focusedItem, focusedCat } = this.state;
|
||||
const { values } = this.store;
|
||||
|
||||
// Don't do anything if no values
|
||||
if (values.length === 0) {
|
||||
if (values.reduce((cur, cat) => cur + cat.values.length, 0) === 0) {
|
||||
return event;
|
||||
}
|
||||
|
||||
@ -423,7 +432,12 @@ class AddressSelect extends Component {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const nextValues = values[focusedCat || 0];
|
||||
const firstCat = values.findIndex((cat) => cat.values.length > 0);
|
||||
const nextCat = focusedCat && values[focusedCat].values.length > 0
|
||||
? focusedCat
|
||||
: firstCat;
|
||||
|
||||
const nextValues = values[nextCat];
|
||||
const nextFocus = nextValues ? nextValues.values[0] : null;
|
||||
return this.focusItem(nextFocus && nextFocus.index || 1);
|
||||
}
|
||||
@ -457,12 +471,21 @@ class AddressSelect extends Component {
|
||||
|
||||
// If right: next category
|
||||
if (direction === 'right') {
|
||||
nextCategory = Math.min(prevCategoryIndex + 1, values.length - 1);
|
||||
const categoryShift = values
|
||||
.slice(prevCategoryIndex + 1, values.length)
|
||||
.findIndex((cat) => cat.values.length > 0) + 1;
|
||||
|
||||
nextCategory = Math.min(prevCategoryIndex + categoryShift, values.length - 1);
|
||||
}
|
||||
|
||||
// If right: previous category
|
||||
if (direction === 'left') {
|
||||
nextCategory = Math.max(prevCategoryIndex - 1, 0);
|
||||
const categoryShift = values
|
||||
.slice(0, prevCategoryIndex)
|
||||
.reverse()
|
||||
.findIndex((cat) => cat.values.length > 0) + 1;
|
||||
|
||||
nextCategory = Math.max(prevCategoryIndex - categoryShift, 0);
|
||||
}
|
||||
|
||||
// If left or right: try to keep the horizontal index
|
||||
@ -486,6 +509,10 @@ class AddressSelect extends Component {
|
||||
}
|
||||
|
||||
handleMainBlur = () => {
|
||||
if (this.props.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.document.hasFocus() && !this.state.expanded) {
|
||||
this.closing = false;
|
||||
this.setState({ focused: false });
|
||||
@ -493,7 +520,7 @@ class AddressSelect extends Component {
|
||||
}
|
||||
|
||||
handleMainFocus = () => {
|
||||
if (this.state.focused) {
|
||||
if (this.state.focused || this.props.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -508,6 +535,12 @@ class AddressSelect extends Component {
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
const { disabled, readOnly } = this.props;
|
||||
|
||||
if (disabled || readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ expanded: true, focusedItem: null, focusedCat: null }, () => {
|
||||
window.setTimeout(() => {
|
||||
this.handleDOMAction(this.inputRef, 'focus');
|
||||
@ -525,43 +558,6 @@ class AddressSelect extends Component {
|
||||
this.setState({ expanded: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the given values based on the given
|
||||
* filter
|
||||
*/
|
||||
filterValues = (values = [], _filter = '') => {
|
||||
const filter = _filter.toLowerCase();
|
||||
|
||||
return values
|
||||
// Remove empty accounts
|
||||
.filter((a) => a)
|
||||
.filter((account) => {
|
||||
const address = account.address.toLowerCase();
|
||||
const inAddress = address.includes(filter);
|
||||
|
||||
if (!account.name || inAddress) {
|
||||
return inAddress;
|
||||
}
|
||||
|
||||
const name = account.name.toLowerCase();
|
||||
const inName = name.includes(filter);
|
||||
const { meta = {} } = account;
|
||||
|
||||
if (!meta.tags || inName) {
|
||||
return inName;
|
||||
}
|
||||
|
||||
const tags = (meta.tags || []).join('');
|
||||
return tags.includes(filter);
|
||||
})
|
||||
.sort((accA, accB) => {
|
||||
const nameA = accA.name || accA.address;
|
||||
const nameB = accB.name || accB.address;
|
||||
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
}
|
||||
|
||||
handleInputBlur = () => {
|
||||
this.setState({ inputFocused: false });
|
||||
}
|
||||
@ -572,25 +568,10 @@ class AddressSelect extends Component {
|
||||
|
||||
handleChange = (event = { target: {} }) => {
|
||||
const { value = '' } = event.target;
|
||||
let index = 0;
|
||||
|
||||
const values = this.values
|
||||
.map((category) => {
|
||||
const filteredValues = this
|
||||
.filterValues(category.values, value)
|
||||
.map((value) => {
|
||||
index++;
|
||||
return { ...value, index: parseInt(index) };
|
||||
});
|
||||
|
||||
return {
|
||||
label: category.label,
|
||||
values: filteredValues
|
||||
};
|
||||
});
|
||||
this.store.handleChange(value);
|
||||
|
||||
this.setState({
|
||||
values,
|
||||
focusedItem: null,
|
||||
inputValue: value
|
||||
});
|
||||
|
218
js/src/ui/Form/AddressSelect/addressSelectStore.js
Normal file
218
js/src/ui/Form/AddressSelect/addressSelectStore.js
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -51,6 +51,7 @@ export default class Input extends Component {
|
||||
PropTypes.string,
|
||||
PropTypes.bool
|
||||
]),
|
||||
autoFocus: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
@ -112,7 +113,7 @@ export default class Input extends Component {
|
||||
|
||||
render () {
|
||||
const { value } = this.state;
|
||||
const { children, className, hideUnderline, disabled, error, focused, label } = this.props;
|
||||
const { autoFocus, children, className, hideUnderline, disabled, error, focused, label } = this.props;
|
||||
const { hint, onClick, onFocus, multiLine, rows, type, min, max, style, tabIndex } = this.props;
|
||||
|
||||
const readOnly = this.props.readOnly || disabled;
|
||||
@ -138,6 +139,7 @@ export default class Input extends Component {
|
||||
{ this.renderCopyButton() }
|
||||
<TextField
|
||||
autoComplete='off'
|
||||
autoFocus={ autoFocus }
|
||||
className={ className }
|
||||
errorText={ error }
|
||||
floatingLabelFixed
|
||||
@ -183,7 +185,7 @@ export default class Input extends Component {
|
||||
|
||||
const text = typeof allowCopy === 'string'
|
||||
? allowCopy
|
||||
: value;
|
||||
: value.toString();
|
||||
|
||||
const style = hideUnderline
|
||||
? {}
|
||||
|
@ -78,7 +78,7 @@ class InputAddress extends Component {
|
||||
return (
|
||||
<div className={ containerClasses.join(' ') }>
|
||||
<Input
|
||||
allowCopy={ allowCopy && (disabled ? value : false) }
|
||||
allowCopy={ allowCopy && ((disabled || readOnly) ? value : false) }
|
||||
className={ classes.join(' ') }
|
||||
disabled={ disabled }
|
||||
error={ error }
|
||||
@ -103,13 +103,13 @@ class InputAddress extends Component {
|
||||
}
|
||||
|
||||
renderIcon () {
|
||||
const { value, disabled, label, allowCopy, hideUnderline } = this.props;
|
||||
const { value, disabled, label, allowCopy, hideUnderline, readOnly } = this.props;
|
||||
|
||||
if (!value || !value.length || !util.isAddressValid(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classes = [disabled ? styles.iconDisabled : styles.icon];
|
||||
const classes = [(disabled || readOnly) ? styles.iconDisabled : styles.icon];
|
||||
|
||||
if (!label) {
|
||||
classes.push(styles.noLabel);
|
||||
|
@ -25,41 +25,44 @@ class InputAddressSelect extends Component {
|
||||
accounts: PropTypes.object.isRequired,
|
||||
contacts: PropTypes.object.isRequired,
|
||||
contracts: PropTypes.object.isRequired,
|
||||
wallets: PropTypes.object.isRequired,
|
||||
|
||||
allowCopy: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
hint: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func
|
||||
label: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
render () {
|
||||
const { accounts, contacts, contracts, wallets, label, hint, error, value, onChange } = this.props;
|
||||
const { accounts, allowCopy, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
|
||||
|
||||
return (
|
||||
<AddressSelect
|
||||
allowCopy={ allowCopy }
|
||||
allowInput
|
||||
accounts={ accounts }
|
||||
contacts={ contacts }
|
||||
contracts={ contracts }
|
||||
wallets={ wallets }
|
||||
error={ error }
|
||||
label={ label }
|
||||
hint={ hint }
|
||||
label={ label }
|
||||
onChange={ onChange }
|
||||
readOnly={ readOnly }
|
||||
value={ value }
|
||||
onChange={ onChange } />
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { accounts, contacts, contracts, wallets } = state.personal;
|
||||
const { accounts, contacts, contracts } = state.personal;
|
||||
|
||||
return {
|
||||
accounts,
|
||||
contacts,
|
||||
contracts,
|
||||
wallets
|
||||
contracts
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -14,19 +14,18 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||
|
||||
import { arrayOrObjectProptype } from '~/util/proptypes';
|
||||
import styles from './radioButtons.css';
|
||||
|
||||
export default class RadioButtons extends Component {
|
||||
static propTypes = {
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
values: PropTypes.array.isRequired,
|
||||
|
||||
value: PropTypes.any,
|
||||
name: PropTypes.string
|
||||
values: arrayOrObjectProptype().isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -40,16 +39,16 @@ export default class RadioButtons extends Component {
|
||||
const index = Number.isNaN(parseInt(value))
|
||||
? values.findIndex((val) => val.key === value)
|
||||
: parseInt(value);
|
||||
|
||||
const selectedValue = typeof value !== 'object' ? values[index] : value;
|
||||
const selectedValue = typeof value !== 'object'
|
||||
? values[index]
|
||||
: value;
|
||||
const key = this.getKey(selectedValue, index);
|
||||
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
valueSelected={ key }
|
||||
name={ name }
|
||||
onChange={ this.onChange }
|
||||
>
|
||||
valueSelected={ key } >
|
||||
{ this.renderContent() }
|
||||
</RadioButtonGroup>
|
||||
);
|
||||
@ -59,7 +58,9 @@ export default class RadioButtons extends Component {
|
||||
const { values } = this.props;
|
||||
|
||||
return values.map((value, index) => {
|
||||
const label = typeof value === 'string' ? value : value.label || '';
|
||||
const label = typeof value === 'string'
|
||||
? value
|
||||
: value.label || '';
|
||||
const description = (typeof value !== 'string' && value.description) || null;
|
||||
const key = this.getKey(value, index);
|
||||
|
||||
@ -67,28 +68,26 @@ export default class RadioButtons extends Component {
|
||||
<RadioButton
|
||||
className={ styles.spaced }
|
||||
key={ index }
|
||||
|
||||
value={ key }
|
||||
label={ (
|
||||
label={
|
||||
<div className={ styles.typeContainer }>
|
||||
<span>{ label }</span>
|
||||
{
|
||||
description
|
||||
? (
|
||||
<span className={ styles.desc }>{ description }</span>
|
||||
)
|
||||
? <span className={ styles.desc }>{ description }</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
) }
|
||||
/>
|
||||
}
|
||||
value={ key } />
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getKey (value, index) {
|
||||
if (typeof value !== 'string') {
|
||||
return typeof value.key === 'undefined' ? index : value.key;
|
||||
return typeof value.key === 'undefined'
|
||||
? index
|
||||
: value.key;
|
||||
}
|
||||
|
||||
return index;
|
||||
@ -96,8 +95,8 @@ export default class RadioButtons extends Component {
|
||||
|
||||
onChange = (event, index) => {
|
||||
const { onChange, values } = this.props;
|
||||
|
||||
const value = values[index] || values.find((v) => v.key === index);
|
||||
|
||||
onChange(value, index);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { MenuItem, Toggle } from 'material-ui';
|
||||
import { range } from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
import IconButton from 'material-ui/IconButton';
|
||||
import AddIcon from 'material-ui/svg-icons/content/add';
|
||||
@ -33,26 +34,31 @@ import styles from './typedInput.css';
|
||||
export default class TypedInput extends Component {
|
||||
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
param: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string
|
||||
]).isRequired,
|
||||
|
||||
accounts: PropTypes.object,
|
||||
allowCopy: PropTypes.bool,
|
||||
error: PropTypes.any,
|
||||
hint: PropTypes.string,
|
||||
isEth: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
max: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
param: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string
|
||||
]).isRequired,
|
||||
onChange: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
value: PropTypes.any
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
allowCopy: false,
|
||||
isEth: null,
|
||||
min: null,
|
||||
max: null,
|
||||
isEth: null
|
||||
onChange: () => {},
|
||||
readOnly: false
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -61,21 +67,16 @@ export default class TypedInput extends Component {
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
if (this.props.isEth && this.props.value) {
|
||||
this.setState({ isEth: true, ethValue: fromWei(this.props.value) });
|
||||
const { isEth, value } = this.props;
|
||||
|
||||
if (typeof isEth === 'boolean' && value) {
|
||||
const ethValue = isEth ? fromWei(value) : value;
|
||||
this.setState({ isEth, ethValue });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { param } = this.props;
|
||||
|
||||
if (typeof param === 'string') {
|
||||
const parsedParam = parseAbiType(param);
|
||||
|
||||
if (parsedParam) {
|
||||
return this.renderParam(parsedParam);
|
||||
}
|
||||
}
|
||||
const param = this.getParam();
|
||||
|
||||
if (param) {
|
||||
return this.renderParam(param);
|
||||
@ -86,7 +87,7 @@ export default class TypedInput extends Component {
|
||||
}
|
||||
|
||||
renderParam (param) {
|
||||
const { isEth } = this.props;
|
||||
const { allowCopy, isEth, readOnly } = this.props;
|
||||
const { type } = param;
|
||||
|
||||
if (type === ABI_TYPES.ARRAY) {
|
||||
@ -104,10 +105,12 @@ export default class TypedInput extends Component {
|
||||
|
||||
return (
|
||||
<TypedInput
|
||||
accounts={ accounts }
|
||||
allowCopy={ allowCopy }
|
||||
key={ `${subtype.type}_${index}` }
|
||||
onChange={ onChange }
|
||||
accounts={ accounts }
|
||||
param={ subtype }
|
||||
readOnly={ readOnly }
|
||||
value={ value[index] }
|
||||
/>
|
||||
);
|
||||
@ -234,20 +237,23 @@ export default class TypedInput extends Component {
|
||||
}
|
||||
|
||||
renderInteger (value = this.props.value, onChange = this.onChange) {
|
||||
const { label, error, param, hint, min, max } = this.props;
|
||||
const { allowCopy, label, error, hint, min, max, readOnly } = this.props;
|
||||
const param = this.getParam();
|
||||
|
||||
const realValue = value && typeof value.toNumber === 'function'
|
||||
? value.toNumber()
|
||||
const realValue = value
|
||||
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
|
||||
: value;
|
||||
|
||||
return (
|
||||
<Input
|
||||
allowCopy={ allowCopy }
|
||||
label={ label }
|
||||
hint={ hint }
|
||||
value={ realValue }
|
||||
error={ error }
|
||||
onChange={ onChange }
|
||||
type='number'
|
||||
readOnly={ readOnly }
|
||||
type={ readOnly ? 'text' : 'number' }
|
||||
step={ 1 }
|
||||
min={ min !== null ? min : (param.signed ? null : 0) }
|
||||
max={ max !== null ? max : null }
|
||||
@ -263,19 +269,22 @@ export default class TypedInput extends Component {
|
||||
* @see https://github.com/facebook/react/issues/1549
|
||||
*/
|
||||
renderFloat (value = this.props.value, onChange = this.onChange) {
|
||||
const { label, error, param, hint, min, max } = this.props;
|
||||
const { allowCopy, label, error, hint, min, max, readOnly } = this.props;
|
||||
const param = this.getParam();
|
||||
|
||||
const realValue = value && typeof value.toNumber === 'function'
|
||||
? value.toNumber()
|
||||
const realValue = value
|
||||
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
|
||||
: value;
|
||||
|
||||
return (
|
||||
<Input
|
||||
allowCopy={ allowCopy }
|
||||
label={ label }
|
||||
hint={ hint }
|
||||
value={ realValue }
|
||||
error={ error }
|
||||
onChange={ onChange }
|
||||
readOnly={ readOnly }
|
||||
type='text'
|
||||
min={ min !== null ? min : (param.signed ? null : 0) }
|
||||
max={ max !== null ? max : null }
|
||||
@ -284,37 +293,44 @@ export default class TypedInput extends Component {
|
||||
}
|
||||
|
||||
renderDefault () {
|
||||
const { label, value, error, hint } = this.props;
|
||||
const { allowCopy, label, value, error, hint, readOnly } = this.props;
|
||||
|
||||
return (
|
||||
<Input
|
||||
allowCopy={ allowCopy }
|
||||
label={ label }
|
||||
hint={ hint }
|
||||
value={ value }
|
||||
error={ error }
|
||||
onSubmit={ this.onSubmit }
|
||||
readOnly={ readOnly }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderAddress () {
|
||||
const { accounts, label, value, error, hint } = this.props;
|
||||
const { accounts, allowCopy, label, value, error, hint, readOnly } = this.props;
|
||||
|
||||
return (
|
||||
<InputAddressSelect
|
||||
allowCopy={ allowCopy }
|
||||
accounts={ accounts }
|
||||
label={ label }
|
||||
hint={ hint }
|
||||
value={ value }
|
||||
error={ error }
|
||||
hint={ hint }
|
||||
label={ label }
|
||||
onChange={ this.onChange }
|
||||
editing
|
||||
readOnly={ readOnly }
|
||||
value={ value }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderBoolean () {
|
||||
const { label, value, error, hint } = this.props;
|
||||
const { allowCopy, label, value, error, hint, readOnly } = this.props;
|
||||
|
||||
if (readOnly) {
|
||||
return this.renderDefault();
|
||||
}
|
||||
|
||||
const boolitems = ['false', 'true'].map((bool) => {
|
||||
return (
|
||||
@ -329,6 +345,7 @@ export default class TypedInput extends Component {
|
||||
|
||||
return (
|
||||
<Select
|
||||
allowCopy={ allowCopy }
|
||||
error={ error }
|
||||
hint={ hint }
|
||||
label={ label }
|
||||
@ -379,7 +396,9 @@ export default class TypedInput extends Component {
|
||||
}
|
||||
|
||||
onAddField = () => {
|
||||
const { value, onChange, param } = this.props;
|
||||
const { value, onChange } = this.props;
|
||||
const param = this.getParam();
|
||||
|
||||
const newValues = [].concat(value, param.subtype.default);
|
||||
|
||||
onChange(newValues);
|
||||
@ -392,4 +411,14 @@ export default class TypedInput extends Component {
|
||||
onChange(newValues);
|
||||
}
|
||||
|
||||
getParam = () => {
|
||||
const { param } = this.props;
|
||||
|
||||
if (typeof param === 'string') {
|
||||
return parseAbiType(param);
|
||||
}
|
||||
|
||||
return param;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import LockedIcon from 'material-ui/svg-icons/action/lock-outline';
|
||||
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
|
||||
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
||||
import SaveIcon from 'material-ui/svg-icons/content/save';
|
||||
import SendIcon from 'material-ui/svg-icons/content/send';
|
||||
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
|
||||
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
|
||||
|
||||
@ -38,6 +39,7 @@ export {
|
||||
NextIcon,
|
||||
PrevIcon,
|
||||
SaveIcon,
|
||||
SendIcon,
|
||||
SnoozeIcon,
|
||||
VisibleIcon
|
||||
};
|
||||
|
@ -118,6 +118,15 @@ export default class MethodDecodingStore {
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
try {
|
||||
const { signature } = this.api.util.decodeCallData(input);
|
||||
|
||||
if (signature === CONTRACT_CREATE || transaction.creates) {
|
||||
result.contract = true;
|
||||
return Promise.resolve({ ...result, deploy: true });
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return this
|
||||
.isContract(contractAddress || transaction.creates)
|
||||
.then((isContract) => {
|
||||
@ -132,7 +141,7 @@ export default class MethodDecodingStore {
|
||||
result.params = paramdata;
|
||||
|
||||
// Contract deployment
|
||||
if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
|
||||
if (!signature) {
|
||||
return Promise.resolve({ ...result, deploy: true });
|
||||
}
|
||||
|
||||
@ -192,7 +201,7 @@ export default class MethodDecodingStore {
|
||||
*/
|
||||
isContract (contractAddress) {
|
||||
// If zero address, it isn't a contract
|
||||
if (/^(0x)?0*$/.test(contractAddress)) {
|
||||
if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ import { Dialog } from 'material-ui';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { nodeOrStringProptype } from '~/util/proptypes';
|
||||
|
||||
@ -49,6 +50,14 @@ class Modal extends Component {
|
||||
waiting: PropTypes.array
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const element = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { muiTheme } = this.context;
|
||||
const { actions, busy, children, className, current, compact, settings, steps, title, visible, waiting } = this.props;
|
||||
@ -85,9 +94,12 @@ class Modal extends Component {
|
||||
<Container
|
||||
compact={ compact }
|
||||
light
|
||||
ref='dialog'
|
||||
style={
|
||||
{ transition: 'none' }
|
||||
}>
|
||||
}
|
||||
tabIndex={ 0 }
|
||||
>
|
||||
{ children }
|
||||
</Container>
|
||||
</Dialog>
|
||||
|
@ -15,6 +15,32 @@
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
$left: 1.5em;
|
||||
$right: $left;
|
||||
$bottom: $left;
|
||||
$top: 20vh;
|
||||
|
||||
.backOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
z-index: -10;
|
||||
opacity: 0;
|
||||
|
||||
transform-origin: 100% 0;
|
||||
transition-property: opacity, z-index;
|
||||
transition-duration: 0.25s;
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
&.expanded {
|
||||
opacity: 1;
|
||||
z-index: 2500;
|
||||
}
|
||||
}
|
||||
|
||||
.parityBackground {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -28,10 +54,10 @@
|
||||
.overlay {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: $top;
|
||||
left: $left;
|
||||
width: calc(100vw - $left - $right);
|
||||
height: calc(100vh - $top - $bottom);
|
||||
|
||||
transform-origin: 100% 0;
|
||||
transition-property: opacity, z-index;
|
||||
@ -48,7 +74,7 @@
|
||||
|
||||
&.expanded {
|
||||
opacity: 1;
|
||||
z-index: 9999;
|
||||
z-index: 3500;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Portal from 'react-portal';
|
||||
import ReactPortal from 'react-portal';
|
||||
import keycode from 'keycode';
|
||||
|
||||
import { CloseIcon } from '~/ui/Icons';
|
||||
@ -24,7 +24,7 @@ import ParityBackground from '~/ui/ParityBackground';
|
||||
|
||||
import styles from './portal.css';
|
||||
|
||||
export default class Protal extends Component {
|
||||
export default class Portal extends Component {
|
||||
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
@ -59,15 +59,19 @@ export default class Protal extends Component {
|
||||
const { children, className } = this.props;
|
||||
|
||||
const classes = [ styles.overlay, className ];
|
||||
const backClasses = [ styles.backOverlay ];
|
||||
|
||||
if (expanded) {
|
||||
classes.push(styles.expanded);
|
||||
backClasses.push(styles.expanded);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal isOpened onClose={ this.handleClose }>
|
||||
<ReactPortal isOpened onClose={ this.handleClose }>
|
||||
<div className={ backClasses.join(' ') } onClick={ this.handleClose }>
|
||||
<div
|
||||
className={ classes.join(' ') }
|
||||
onClick={ this.stopEvent }
|
||||
onKeyDown={ this.handleKeyDown }
|
||||
>
|
||||
<ParityBackground className={ styles.parityBackground } />
|
||||
@ -75,7 +79,8 @@ export default class Protal extends Component {
|
||||
{ this.renderCloseIcon() }
|
||||
{ children }
|
||||
</div>
|
||||
</Portal>
|
||||
</div>
|
||||
</ReactPortal>
|
||||
);
|
||||
}
|
||||
|
||||
@ -93,6 +98,11 @@ export default class Protal extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
stopEvent = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
@ -16,13 +16,15 @@
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { arrayOrObjectProptype } from '~/util/proptypes';
|
||||
|
||||
import styles from './tags.css';
|
||||
|
||||
export default class Tags extends Component {
|
||||
static propTypes = {
|
||||
handleAddSearchToken: PropTypes.func,
|
||||
setRefs: PropTypes.func,
|
||||
tags: PropTypes.array
|
||||
tags: arrayOrObjectProptype()
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -134,7 +134,7 @@ class TxHash extends Component {
|
||||
const { api } = this.context;
|
||||
const { hash } = this.props;
|
||||
|
||||
if (error) {
|
||||
if (error || !hash || /^(0x)?0*$/.test(hash)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,13 @@
|
||||
|
||||
import { PropTypes } from 'react';
|
||||
|
||||
export function arrayOrObjectProptype () {
|
||||
return PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
]);
|
||||
}
|
||||
|
||||
export function nullableProptype (type) {
|
||||
return PropTypes.oneOfType([
|
||||
PropTypes.oneOf([ null ]),
|
||||
|
@ -14,10 +14,93 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import WalletsUtils from '~/util/wallets';
|
||||
|
||||
const isValidReceipt = (receipt) => {
|
||||
return receipt && receipt.blockNumber && receipt.blockNumber.gt(0);
|
||||
};
|
||||
|
||||
function getTxArgs (func, options, values = []) {
|
||||
const { contract } = func;
|
||||
const { api } = contract;
|
||||
const address = options.from;
|
||||
|
||||
if (!address) {
|
||||
return Promise.resolve({ func, options, values });
|
||||
}
|
||||
|
||||
return WalletsUtils
|
||||
.isWallet(api, address)
|
||||
.then((isWallet) => {
|
||||
if (!isWallet) {
|
||||
return { func, options, values };
|
||||
}
|
||||
|
||||
options.data = contract.getCallData(func, options, values);
|
||||
options.to = options.to || contract.address;
|
||||
|
||||
if (!options.to) {
|
||||
return { func, options, values };
|
||||
}
|
||||
|
||||
return WalletsUtils
|
||||
.getCallArgs(api, options, values)
|
||||
.then((callArgs) => {
|
||||
if (!callArgs) {
|
||||
return { func, options, values };
|
||||
}
|
||||
|
||||
return callArgs;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function estimateGas (_func, _options, _values = []) {
|
||||
return getTxArgs(_func, _options, _values)
|
||||
.then((callArgs) => {
|
||||
const { func, options, values } = callArgs;
|
||||
return func._estimateGas(options, values);
|
||||
})
|
||||
.then((gas) => {
|
||||
return WalletsUtils
|
||||
.isWallet(_func.contract.api, _options.from)
|
||||
.then((isWallet) => {
|
||||
if (isWallet) {
|
||||
return gas.mul(1.5);
|
||||
}
|
||||
|
||||
return gas;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function postTransaction (_func, _options, _values = []) {
|
||||
return getTxArgs(_func, _options, _values)
|
||||
.then((callArgs) => {
|
||||
const { func, options, values } = callArgs;
|
||||
return func._postTransaction(options, values);
|
||||
});
|
||||
}
|
||||
|
||||
export function patchApi (api) {
|
||||
api.patch = {
|
||||
...api.patch,
|
||||
contract: patchContract
|
||||
};
|
||||
}
|
||||
|
||||
export function patchContract (contract) {
|
||||
contract._functions.forEach((func) => {
|
||||
if (!func.constant) {
|
||||
func._postTransaction = func.postTransaction;
|
||||
func._estimateGas = func.estimateGas;
|
||||
|
||||
func.postTransaction = postTransaction.bind(contract, func);
|
||||
func.estimateGas = estimateGas.bind(contract, func);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function checkIfTxFailed (api, tx, gasSent) {
|
||||
return api.pollMethod('eth_getTransactionReceipt', tx)
|
||||
.then((receipt) => {
|
||||
|
@ -35,21 +35,21 @@ export const ERRORS = {
|
||||
gasBlockLimit: 'the transaction execution will exceed the block gas limit'
|
||||
};
|
||||
|
||||
export function validateAbi (abi, api) {
|
||||
export function validateAbi (abi) {
|
||||
let abiError = null;
|
||||
let abiParsed = null;
|
||||
|
||||
try {
|
||||
abiParsed = JSON.parse(abi);
|
||||
|
||||
if (!api.util.isArray(abiParsed)) {
|
||||
if (!util.isArray(abiParsed)) {
|
||||
abiError = ERRORS.invalidAbi;
|
||||
return { abi, abiError, abiParsed };
|
||||
}
|
||||
|
||||
// Validate each elements of the Array
|
||||
const invalidIndex = abiParsed
|
||||
.map((o) => isValidAbiEvent(o, api) || isValidAbiFunction(o, api) || isAbiFallback(o))
|
||||
.map((o) => isValidAbiEvent(o) || isValidAbiFunction(o) || isAbiFallback(o))
|
||||
.findIndex((valid) => !valid);
|
||||
|
||||
if (invalidIndex !== -1) {
|
||||
@ -70,13 +70,13 @@ export function validateAbi (abi, api) {
|
||||
};
|
||||
}
|
||||
|
||||
function isValidAbiFunction (object, api) {
|
||||
function isValidAbiFunction (object) {
|
||||
if (!object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((object.type === 'function' && object.name) || object.type === 'constructor') &&
|
||||
(object.inputs && api.util.isArray(object.inputs));
|
||||
(object.inputs && util.isArray(object.inputs));
|
||||
}
|
||||
|
||||
function isAbiFallback (object) {
|
||||
@ -87,14 +87,14 @@ function isAbiFallback (object) {
|
||||
return object.type === 'fallback';
|
||||
}
|
||||
|
||||
function isValidAbiEvent (object, api) {
|
||||
function isValidAbiEvent (object) {
|
||||
if (!object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (object.type === 'event') &&
|
||||
(object.name) &&
|
||||
(object.inputs && api.util.isArray(object.inputs));
|
||||
(object.inputs && util.isArray(object.inputs));
|
||||
}
|
||||
|
||||
export function validateAddress (address) {
|
||||
|
@ -14,13 +14,92 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { range, uniq } from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { intersection, range, uniq } from 'lodash';
|
||||
|
||||
import Contract from '~/api/contract';
|
||||
import { bytesToHex, toHex } from '~/api/util/format';
|
||||
import { validateAddress } from '~/util/validation';
|
||||
import WalletAbi from '~/contracts/abi/wallet.json';
|
||||
|
||||
const _cachedWalletLookup = {};
|
||||
|
||||
export default class WalletsUtils {
|
||||
|
||||
static getCallArgs (api, options, values = []) {
|
||||
const walletContract = new Contract(api, WalletAbi);
|
||||
|
||||
const promises = [
|
||||
api.parity.accountsInfo(),
|
||||
WalletsUtils.fetchOwners(walletContract.at(options.from))
|
||||
];
|
||||
|
||||
return Promise
|
||||
.all(promises)
|
||||
.then(([ accounts, owners ]) => {
|
||||
const addresses = Object.keys(accounts);
|
||||
const owner = intersection(addresses, owners).pop();
|
||||
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return owner;
|
||||
})
|
||||
.then((owner) => {
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const _options = Object.assign({}, options);
|
||||
const { from, to, value = new BigNumber(0), data } = options;
|
||||
|
||||
delete _options.data;
|
||||
|
||||
const nextValues = [ to, value, data ];
|
||||
const nextOptions = {
|
||||
..._options,
|
||||
from: owner,
|
||||
to: from,
|
||||
value: new BigNumber(0)
|
||||
};
|
||||
|
||||
const execFunc = walletContract.instance.execute;
|
||||
|
||||
return { func: execFunc, options: nextOptions, values: nextValues };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given address could be
|
||||
* a Wallet. The result is cached in order not
|
||||
* to make unnecessary calls on non-wallet accounts
|
||||
*/
|
||||
static isWallet (api, address) {
|
||||
if (!_cachedWalletLookup[address]) {
|
||||
const walletContract = new Contract(api, WalletAbi);
|
||||
|
||||
_cachedWalletLookup[address] = walletContract
|
||||
.at(address)
|
||||
.instance
|
||||
.m_numOwners
|
||||
.call()
|
||||
.then((result) => {
|
||||
if (!result || result.equals(0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.then((bool) => {
|
||||
_cachedWalletLookup[address] = Promise.resolve(bool);
|
||||
return bool;
|
||||
});
|
||||
}
|
||||
|
||||
return _cachedWalletLookup[address];
|
||||
}
|
||||
|
||||
static fetchRequire (walletContract) {
|
||||
return walletContract.instance.m_required.call();
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ export default class Header extends Component {
|
||||
render () {
|
||||
const { account, balance, className, children, hideName } = this.props;
|
||||
const { address, meta, uuid } = account;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ class List extends Component {
|
||||
order: PropTypes.string,
|
||||
orderFallback: PropTypes.string,
|
||||
search: PropTypes.array,
|
||||
walletsOwners: PropTypes.object,
|
||||
|
||||
fetchCertifiers: PropTypes.func.isRequired,
|
||||
fetchCertifications: PropTypes.func.isRequired,
|
||||
@ -58,7 +57,7 @@ class List extends Component {
|
||||
}
|
||||
|
||||
renderAccounts () {
|
||||
const { accounts, balances, empty, link, walletsOwners, handleAddSearchToken } = this.props;
|
||||
const { accounts, balances, empty, link, handleAddSearchToken } = this.props;
|
||||
|
||||
if (empty) {
|
||||
return (
|
||||
@ -76,7 +75,7 @@ class List extends Component {
|
||||
const account = accounts[address] || {};
|
||||
const balance = balances[address] || {};
|
||||
|
||||
const owners = walletsOwners && walletsOwners[address] || null;
|
||||
const owners = account.owners || null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -157,7 +157,11 @@ export default class Summary extends Component {
|
||||
const { link, noLink, account, name } = this.props;
|
||||
|
||||
const { address } = account;
|
||||
const viewLink = `/${link || 'accounts'}/${address}`;
|
||||
const baseLink = account.wallet
|
||||
? 'wallet'
|
||||
: link || 'accounts';
|
||||
|
||||
const viewLink = `/${baseLink}/${address}`;
|
||||
|
||||
const content = (
|
||||
<IdentityName address={ address } name={ name } unknown />
|
||||
|
@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import ContentAdd from 'material-ui/svg-icons/content/add';
|
||||
import { uniq, isEqual } from 'lodash';
|
||||
import { uniq, isEqual, pickBy, omitBy } from 'lodash';
|
||||
|
||||
import List from './List';
|
||||
import { CreateAccount, CreateWallet } from '~/modals';
|
||||
@ -36,9 +36,6 @@ class Accounts extends Component {
|
||||
setVisibleAccounts: PropTypes.func.isRequired,
|
||||
accounts: PropTypes.object.isRequired,
|
||||
hasAccounts: PropTypes.bool.isRequired,
|
||||
wallets: PropTypes.object.isRequired,
|
||||
walletsOwners: PropTypes.object.isRequired,
|
||||
hasWallets: PropTypes.bool.isRequired,
|
||||
|
||||
balances: PropTypes.object
|
||||
}
|
||||
@ -62,8 +59,8 @@ class Accounts extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const prevAddresses = Object.keys({ ...this.props.accounts, ...this.props.wallets });
|
||||
const nextAddresses = Object.keys({ ...nextProps.accounts, ...nextProps.wallets });
|
||||
const prevAddresses = Object.keys(this.props.accounts);
|
||||
const nextAddresses = Object.keys(nextProps.accounts);
|
||||
|
||||
if (prevAddresses.length !== nextAddresses.length || !isEqual(prevAddresses.sort(), nextAddresses.sort())) {
|
||||
this.setVisibleAccounts(nextProps);
|
||||
@ -75,8 +72,8 @@ class Accounts extends Component {
|
||||
}
|
||||
|
||||
setVisibleAccounts (props = this.props) {
|
||||
const { accounts, wallets, setVisibleAccounts } = props;
|
||||
const addresses = Object.keys({ ...accounts, ...wallets });
|
||||
const { accounts, setVisibleAccounts } = props;
|
||||
const addresses = Object.keys(accounts);
|
||||
setVisibleAccounts(addresses);
|
||||
}
|
||||
|
||||
@ -115,30 +112,38 @@ class Accounts extends Component {
|
||||
}
|
||||
|
||||
renderAccounts () {
|
||||
const { accounts, balances } = this.props;
|
||||
|
||||
const _accounts = omitBy(accounts, (a) => a.wallet);
|
||||
const _hasAccounts = Object.keys(_accounts).length > 0;
|
||||
|
||||
if (!this.state.show) {
|
||||
return this.renderLoading(this.props.accounts);
|
||||
return this.renderLoading(_accounts);
|
||||
}
|
||||
|
||||
const { accounts, hasAccounts, balances } = this.props;
|
||||
const { searchValues, sortOrder } = this.state;
|
||||
|
||||
return (
|
||||
<List
|
||||
search={ searchValues }
|
||||
accounts={ accounts }
|
||||
accounts={ _accounts }
|
||||
balances={ balances }
|
||||
empty={ !hasAccounts }
|
||||
empty={ !_hasAccounts }
|
||||
order={ sortOrder }
|
||||
handleAddSearchToken={ this.onAddSearchToken } />
|
||||
);
|
||||
}
|
||||
|
||||
renderWallets () {
|
||||
const { accounts, balances } = this.props;
|
||||
|
||||
const wallets = pickBy(accounts, (a) => a.wallet);
|
||||
const hasWallets = Object.keys(wallets).length > 0;
|
||||
|
||||
if (!this.state.show) {
|
||||
return this.renderLoading(this.props.wallets);
|
||||
return this.renderLoading(wallets);
|
||||
}
|
||||
|
||||
const { wallets, hasWallets, balances, walletsOwners } = this.props;
|
||||
const { searchValues, sortOrder } = this.state;
|
||||
|
||||
if (!wallets || Object.keys(wallets).length === 0) {
|
||||
@ -154,7 +159,6 @@ class Accounts extends Component {
|
||||
empty={ !hasWallets }
|
||||
order={ sortOrder }
|
||||
handleAddSearchToken={ this.onAddSearchToken }
|
||||
walletsOwners={ walletsOwners }
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -287,34 +291,12 @@ class Accounts extends Component {
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { accounts, hasAccounts, wallets, hasWallets, accountsInfo } = state.personal;
|
||||
const { accounts, hasAccounts } = state.personal;
|
||||
const { balances } = state.balances;
|
||||
const walletsInfo = state.wallet.wallets;
|
||||
|
||||
const walletsOwners = Object
|
||||
.keys(walletsInfo)
|
||||
.map((wallet) => {
|
||||
const owners = walletsInfo[wallet].owners || [];
|
||||
|
||||
return {
|
||||
owners: owners.map((owner) => ({
|
||||
address: owner,
|
||||
name: accountsInfo[owner] && accountsInfo[owner].name || owner
|
||||
})),
|
||||
address: wallet
|
||||
};
|
||||
})
|
||||
.reduce((walletsOwners, wallet) => {
|
||||
walletsOwners[wallet.address] = wallet.owners;
|
||||
return walletsOwners;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
accounts,
|
||||
hasAccounts,
|
||||
wallets,
|
||||
walletsOwners,
|
||||
hasWallets,
|
||||
accounts: accounts,
|
||||
hasAccounts: hasAccounts,
|
||||
balances
|
||||
};
|
||||
}
|
||||
|
@ -19,29 +19,31 @@ import React, { Component, PropTypes } from 'react';
|
||||
import LinearProgress from 'material-ui/LinearProgress';
|
||||
import { Card, CardActions, CardTitle, CardText } from 'material-ui/Card';
|
||||
|
||||
import { Button, Input, InputAddress, InputAddressSelect } from '~/ui';
|
||||
import { Button, TypedInput } from '~/ui';
|
||||
import { arrayOrObjectProptype } from '~/util/proptypes';
|
||||
|
||||
import styles from './queries.css';
|
||||
|
||||
export default class InputQuery extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object
|
||||
}
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
accountsInfo: PropTypes.object.isRequired,
|
||||
contract: PropTypes.object.isRequired,
|
||||
inputs: PropTypes.array.isRequired,
|
||||
outputs: PropTypes.array.isRequired,
|
||||
inputs: arrayOrObjectProptype().isRequired,
|
||||
outputs: arrayOrObjectProptype().isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
signature: PropTypes.string.isRequired,
|
||||
className: PropTypes.string
|
||||
}
|
||||
};
|
||||
|
||||
state = {
|
||||
isValid: true,
|
||||
results: [],
|
||||
values: {}
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { name, className } = this.props;
|
||||
@ -89,7 +91,7 @@ export default class InputQuery extends Component {
|
||||
|
||||
renderResults () {
|
||||
const { results, isLoading } = this.state;
|
||||
const { outputs } = this.props;
|
||||
const { accountsInfo, outputs } = this.props;
|
||||
|
||||
if (isLoading) {
|
||||
return (<LinearProgress mode='indeterminate' />);
|
||||
@ -108,25 +110,16 @@ export default class InputQuery extends Component {
|
||||
}))
|
||||
.sort((outA, outB) => outA.display.length - outB.display.length)
|
||||
.map((out, index) => {
|
||||
let input = null;
|
||||
if (out.type === 'address') {
|
||||
input = (
|
||||
<InputAddress
|
||||
className={ styles.queryValue }
|
||||
disabled
|
||||
value={ out.display }
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
input = (
|
||||
<Input
|
||||
className={ styles.queryValue }
|
||||
readOnly
|
||||
const input = (
|
||||
<TypedInput
|
||||
accounts={ accountsInfo }
|
||||
allowCopy
|
||||
isEth={ false }
|
||||
param={ out.type }
|
||||
readOnly
|
||||
value={ out.display }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={ index }>
|
||||
@ -144,8 +137,7 @@ export default class InputQuery extends Component {
|
||||
const { name, type } = input;
|
||||
const label = `${name ? `${name}: ` : ''}${type}`;
|
||||
|
||||
const onChange = (event, input) => {
|
||||
const value = event && event.target.value || input;
|
||||
const onChange = (value) => {
|
||||
const { values } = this.state;
|
||||
|
||||
this.setState({
|
||||
@ -156,28 +148,15 @@ export default class InputQuery extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
if (type === 'address') {
|
||||
return (
|
||||
<div key={ name }>
|
||||
<InputAddressSelect
|
||||
<TypedInput
|
||||
hint={ type }
|
||||
label={ label }
|
||||
value={ values[name] }
|
||||
required
|
||||
isEth={ false }
|
||||
onChange={ onChange }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={ name }>
|
||||
<Input
|
||||
hint={ type }
|
||||
label={ label }
|
||||
param={ type }
|
||||
value={ values[name] }
|
||||
required
|
||||
onChange={ onChange }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -192,7 +171,9 @@ export default class InputQuery extends Component {
|
||||
|
||||
if (api.util.isInstanceOf(value, BigNumber)) {
|
||||
return value.toFormat(0);
|
||||
} else if (api.util.isArray(value)) {
|
||||
}
|
||||
|
||||
if (api.util.isArray(value)) {
|
||||
return api.util.bytesToHex(value);
|
||||
}
|
||||
|
||||
|
@ -14,12 +14,11 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Card, CardTitle, CardText } from 'material-ui/Card';
|
||||
|
||||
import InputQuery from './inputQuery';
|
||||
import { Container, Input, InputAddress } from '~/ui';
|
||||
import { Container, TypedInput } from '~/ui';
|
||||
|
||||
import styles from './queries.css';
|
||||
|
||||
@ -29,6 +28,7 @@ export default class Queries extends Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
accountsInfo: PropTypes.object.isRequired,
|
||||
contract: PropTypes.object,
|
||||
values: PropTypes.object
|
||||
}
|
||||
@ -74,11 +74,12 @@ export default class Queries extends Component {
|
||||
|
||||
renderInputQuery (fn) {
|
||||
const { abi, name, signature } = fn;
|
||||
const { contract } = this.props;
|
||||
const { accountsInfo, contract } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ styles.container } key={ fn.signature }>
|
||||
<InputQuery
|
||||
accountsInfo={ accountsInfo }
|
||||
className={ styles.method }
|
||||
inputs={ abi.inputs }
|
||||
outputs={ abi.outputs }
|
||||
@ -116,34 +117,23 @@ export default class Queries extends Component {
|
||||
}
|
||||
|
||||
const { api } = this.context;
|
||||
let valueToDisplay = null;
|
||||
const { accountsInfo } = this.props;
|
||||
|
||||
if (api.util.isInstanceOf(value, BigNumber)) {
|
||||
valueToDisplay = value.toFormat(0);
|
||||
} else if (api.util.isArray(value)) {
|
||||
let valueToDisplay = value;
|
||||
|
||||
if (api.util.isArray(value)) {
|
||||
valueToDisplay = api.util.bytesToHex(value);
|
||||
} else if (typeof value === 'boolean') {
|
||||
valueToDisplay = value ? 'true' : 'false';
|
||||
} else {
|
||||
valueToDisplay = value.toString();
|
||||
}
|
||||
|
||||
if (type === 'address') {
|
||||
return (
|
||||
<InputAddress
|
||||
className={ styles.queryValue }
|
||||
value={ valueToDisplay }
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={ styles.queryValue }
|
||||
value={ valueToDisplay }
|
||||
readOnly
|
||||
<TypedInput
|
||||
accounts={ accountsInfo }
|
||||
allowCopy
|
||||
isEth={ false }
|
||||
param={ type }
|
||||
readOnly
|
||||
value={ valueToDisplay }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ class Contract extends Component {
|
||||
setVisibleAccounts: PropTypes.func.isRequired,
|
||||
|
||||
accounts: PropTypes.object,
|
||||
accountsInfo: PropTypes.object,
|
||||
balances: PropTypes.object,
|
||||
contracts: PropTypes.object,
|
||||
isTest: PropTypes.bool,
|
||||
@ -115,7 +116,7 @@ class Contract extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { balances, contracts, params, isTest } = this.props;
|
||||
const { accountsInfo, balances, contracts, params, isTest } = this.props;
|
||||
const { allEvents, contract, queryValues, loadingEvents } = this.state;
|
||||
const account = contracts[params.address];
|
||||
const balance = balances[params.address];
|
||||
@ -138,6 +139,7 @@ class Contract extends Component {
|
||||
/>
|
||||
|
||||
<Queries
|
||||
accountsInfo={ accountsInfo }
|
||||
contract={ contract }
|
||||
values={ queryValues }
|
||||
/>
|
||||
@ -447,13 +449,14 @@ class Contract extends Component {
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { accounts, contracts } = state.personal;
|
||||
const { accounts, accountsInfo, contracts } = state.personal;
|
||||
const { balances } = state.balances;
|
||||
const { isTest } = state.nodeStatus;
|
||||
|
||||
return {
|
||||
isTest,
|
||||
accounts,
|
||||
accountsInfo,
|
||||
contracts,
|
||||
balances
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { omitBy } from 'lodash';
|
||||
|
||||
import { AddDapps, DappPermissions } from '~/modals';
|
||||
import PermissionStore from '~/modals/DappPermissions/store';
|
||||
@ -150,8 +151,15 @@ class Dapps extends Component {
|
||||
function mapStateToProps (state) {
|
||||
const { accounts } = state.personal;
|
||||
|
||||
/**
|
||||
* Do not show the Wallet Accounts in the Dapps
|
||||
* Permissions Modal. This will come in v1.6, but
|
||||
* for now it would break dApps using Web3...
|
||||
*/
|
||||
const _accounts = omitBy(accounts, (account) => account.wallet);
|
||||
|
||||
return {
|
||||
accounts
|
||||
accounts: _accounts
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ export default class RequestPending extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
date: PropTypes.instanceOf(Date).isRequired,
|
||||
focus: PropTypes.bool,
|
||||
gasLimit: PropTypes.object.isRequired,
|
||||
id: PropTypes.object.isRequired,
|
||||
isSending: PropTypes.bool.isRequired,
|
||||
@ -38,6 +39,7 @@ export default class RequestPending extends Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
focus: false,
|
||||
isSending: false
|
||||
};
|
||||
|
||||
@ -49,7 +51,7 @@ export default class RequestPending extends Component {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { className, date, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props;
|
||||
const { className, date, focus, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props;
|
||||
|
||||
if (payload.sign) {
|
||||
const { sign } = payload;
|
||||
@ -58,6 +60,7 @@ export default class RequestPending extends Component {
|
||||
<SignRequest
|
||||
address={ sign.address }
|
||||
className={ className }
|
||||
focus={ focus }
|
||||
hash={ sign.hash }
|
||||
id={ id }
|
||||
isFinished={ false }
|
||||
@ -75,6 +78,7 @@ export default class RequestPending extends Component {
|
||||
<TransactionPending
|
||||
className={ className }
|
||||
date={ date }
|
||||
focus={ focus }
|
||||
gasLimit={ gasLimit }
|
||||
id={ id }
|
||||
isSending={ isSending }
|
||||
|
@ -30,13 +30,19 @@ export default class SignRequest extends Component {
|
||||
address: PropTypes.string.isRequired,
|
||||
hash: PropTypes.string.isRequired,
|
||||
isFinished: PropTypes.bool.isRequired,
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
store: PropTypes.object.isRequired,
|
||||
|
||||
className: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
isSending: PropTypes.bool,
|
||||
onConfirm: PropTypes.func,
|
||||
onReject: PropTypes.func,
|
||||
status: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
store: PropTypes.object.isRequired
|
||||
status: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
focus: false
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
@ -81,7 +87,7 @@ export default class SignRequest extends Component {
|
||||
}
|
||||
|
||||
renderActions () {
|
||||
const { address, isFinished, status } = this.props;
|
||||
const { address, focus, isFinished, status } = this.props;
|
||||
|
||||
if (isFinished) {
|
||||
if (status === 'confirmed') {
|
||||
@ -111,6 +117,7 @@ export default class SignRequest extends Component {
|
||||
return (
|
||||
<TransactionPendingForm
|
||||
address={ address }
|
||||
focus={ focus }
|
||||
isSending={ this.props.isSending }
|
||||
onConfirm={ this.onConfirm }
|
||||
onReject={ this.onReject }
|
||||
|
@ -35,6 +35,7 @@ export default class TransactionPending extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
date: PropTypes.instanceOf(Date).isRequired,
|
||||
focus: PropTypes.bool,
|
||||
gasLimit: PropTypes.object,
|
||||
id: PropTypes.object.isRequired,
|
||||
isSending: PropTypes.bool.isRequired,
|
||||
@ -53,6 +54,10 @@ export default class TransactionPending extends Component {
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
focus: false
|
||||
};
|
||||
|
||||
gasStore = new GasPriceEditor.Store(this.context.api, {
|
||||
gas: this.props.transaction.gas.toFixed(),
|
||||
gasLimit: this.props.gasLimit,
|
||||
@ -80,7 +85,7 @@ export default class TransactionPending extends Component {
|
||||
}
|
||||
|
||||
renderTransaction () {
|
||||
const { className, id, isSending, isTest, store, transaction } = this.props;
|
||||
const { className, focus, id, isSending, isTest, store, transaction } = this.props;
|
||||
const { totalValue } = this.state;
|
||||
const { from, value } = transaction;
|
||||
|
||||
@ -100,6 +105,7 @@ export default class TransactionPending extends Component {
|
||||
value={ value } />
|
||||
<TransactionPendingForm
|
||||
address={ from }
|
||||
focus={ focus }
|
||||
isSending={ isSending }
|
||||
onConfirm={ this.onConfirm }
|
||||
onReject={ this.onReject } />
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
@ -26,11 +27,16 @@ import styles from './transactionPendingFormConfirm.css';
|
||||
|
||||
class TransactionPendingFormConfirm extends Component {
|
||||
static propTypes = {
|
||||
accounts: PropTypes.object.isRequired,
|
||||
account: PropTypes.object.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
isSending: PropTypes.bool.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired
|
||||
}
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
focus: PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
focus: false
|
||||
};
|
||||
|
||||
id = Math.random(); // for tooltip
|
||||
|
||||
@ -40,10 +46,39 @@ class TransactionPendingFormConfirm extends Component {
|
||||
walletError: null
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!this.props.focus && nextProps.focus) {
|
||||
this.focus(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Properly focus on the input element when needed.
|
||||
* This might be fixed some day in MaterialUI with
|
||||
* an autoFocus prop.
|
||||
*
|
||||
* @see https://github.com/callemall/material-ui/issues/5632
|
||||
*/
|
||||
focus (props = this.props) {
|
||||
if (props.focus) {
|
||||
const textNode = ReactDOM.findDOMNode(this.refs.input);
|
||||
|
||||
if (!textNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputNode = textNode.querySelector('input');
|
||||
inputNode && inputNode.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accounts, address, isSending } = this.props;
|
||||
const { account, address, isSending } = this.props;
|
||||
const { password, wallet, walletError } = this.state;
|
||||
const account = accounts[address] || {};
|
||||
const isExternal = !account.uuid;
|
||||
|
||||
const passwordHint = account.meta && account.meta.passwordHint
|
||||
@ -72,8 +107,10 @@ class TransactionPendingFormConfirm extends Component {
|
||||
}
|
||||
onChange={ this.onModifyPassword }
|
||||
onKeyDown={ this.onKeyDown }
|
||||
ref='input'
|
||||
type='password'
|
||||
value={ password } />
|
||||
value={ password }
|
||||
/>
|
||||
<div className={ styles.passwordHint }>
|
||||
{ passwordHint }
|
||||
</div>
|
||||
@ -178,11 +215,14 @@ class TransactionPendingFormConfirm extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { accounts } = state.personal;
|
||||
function mapStateToProps (initState, initProps) {
|
||||
const { accounts } = initState.personal;
|
||||
const { address } = initProps;
|
||||
|
||||
return {
|
||||
accounts
|
||||
const account = accounts[address] || {};
|
||||
|
||||
return () => {
|
||||
return { account };
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,12 @@ export default class TransactionPendingForm extends Component {
|
||||
isSending: PropTypes.bool.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onReject: PropTypes.func.isRequired,
|
||||
className: PropTypes.string
|
||||
className: PropTypes.string,
|
||||
focus: PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
focus: false
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -47,7 +52,7 @@ export default class TransactionPendingForm extends Component {
|
||||
}
|
||||
|
||||
renderForm () {
|
||||
const { address, isSending, onConfirm, onReject } = this.props;
|
||||
const { address, focus, isSending, onConfirm, onReject } = this.props;
|
||||
|
||||
if (this.state.isRejectOpen) {
|
||||
return (
|
||||
@ -59,8 +64,10 @@ export default class TransactionPendingForm extends Component {
|
||||
return (
|
||||
<TransactionPendingFormConfirm
|
||||
address={ address }
|
||||
focus={ focus }
|
||||
isSending={ isSending }
|
||||
onConfirm={ onConfirm } />
|
||||
onConfirm={ onConfirm }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ class Embedded extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderPending = (data) => {
|
||||
renderPending = (data, index) => {
|
||||
const { actions, gasLimit, isTest } = this.props;
|
||||
const { date, id, isSending, payload } = data;
|
||||
|
||||
@ -86,6 +86,7 @@ class Embedded extends Component {
|
||||
<RequestPending
|
||||
className={ styles.request }
|
||||
date={ date }
|
||||
focus={ index === 0 }
|
||||
gasLimit={ gasLimit }
|
||||
id={ id }
|
||||
isSending={ isSending }
|
||||
|
@ -104,7 +104,7 @@ class RequestsPage extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderPending = (data) => {
|
||||
renderPending = (data, index) => {
|
||||
const { actions, gasLimit, isTest } = this.props;
|
||||
const { date, id, isSending, payload } = data;
|
||||
|
||||
@ -112,6 +112,7 @@ class RequestsPage extends Component {
|
||||
<RequestPending
|
||||
className={ styles.request }
|
||||
date={ date }
|
||||
focus={ index === 0 }
|
||||
gasLimit={ gasLimit }
|
||||
id={ id }
|
||||
isSending={ isSending }
|
||||
|
@ -55,14 +55,20 @@ export default class WalletDetails extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ownersList = owners.map((address, idx) => (
|
||||
const ownersList = owners.map((owner, idx) => {
|
||||
const address = typeof owner === 'object'
|
||||
? owner.address
|
||||
: owner;
|
||||
|
||||
return (
|
||||
<InputAddress
|
||||
key={ `${idx}_${address}` }
|
||||
value={ address }
|
||||
disabled
|
||||
text
|
||||
/>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -57,12 +57,12 @@ export default class WalletTransactions extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
const txRows = transactions.map((transaction) => {
|
||||
const txRows = transactions.slice(0, 15).map((transaction, index) => {
|
||||
const { transactionHash, blockNumber, from, to, value, data } = transaction;
|
||||
|
||||
return (
|
||||
<TxRow
|
||||
key={ transactionHash }
|
||||
key={ `${transactionHash}_${index}` }
|
||||
tx={ {
|
||||
hash: transactionHash,
|
||||
input: data && bytesToHex(data) || '',
|
||||
|
@ -64,13 +64,14 @@ class Wallet extends Component {
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
setVisibleAccounts: PropTypes.func.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
balance: nullableProptype(PropTypes.object.isRequired),
|
||||
images: PropTypes.object.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
wallets: PropTypes.object.isRequired,
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
owned: PropTypes.bool.isRequired,
|
||||
setVisibleAccounts: PropTypes.func.isRequired,
|
||||
wallet: PropTypes.object.isRequired,
|
||||
isTest: PropTypes.bool.isRequired
|
||||
walletAccount: nullableProptype(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -104,28 +105,26 @@ class Wallet extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { wallets, balance, address } = this.props;
|
||||
const { walletAccount, balance, wallet } = this.props;
|
||||
|
||||
const wallet = (wallets || {})[address];
|
||||
|
||||
if (!wallet) {
|
||||
if (!walletAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { owners, require, dailylimit } = this.props.wallet;
|
||||
const { owners, require, dailylimit } = wallet;
|
||||
|
||||
return (
|
||||
<div className={ styles.wallet }>
|
||||
{ this.renderEditDialog(wallet) }
|
||||
{ this.renderEditDialog(walletAccount) }
|
||||
{ this.renderSettingsDialog() }
|
||||
{ this.renderTransferDialog() }
|
||||
{ this.renderDeleteDialog(wallet) }
|
||||
{ this.renderDeleteDialog(walletAccount) }
|
||||
{ this.renderActionbar() }
|
||||
<Page>
|
||||
<div className={ styles.info }>
|
||||
<Header
|
||||
className={ styles.header }
|
||||
account={ wallet }
|
||||
account={ walletAccount }
|
||||
balance={ balance }
|
||||
isContract
|
||||
>
|
||||
@ -209,32 +208,47 @@ class Wallet extends Component {
|
||||
}
|
||||
|
||||
renderActionbar () {
|
||||
const { balance } = this.props;
|
||||
const { balance, owned } = this.props;
|
||||
const showTransferButton = !!(balance && balance.tokens);
|
||||
|
||||
const buttons = [
|
||||
const buttons = [];
|
||||
|
||||
if (owned) {
|
||||
buttons.push(
|
||||
<Button
|
||||
key='transferFunds'
|
||||
icon={ <ContentSend /> }
|
||||
label='transfer'
|
||||
disabled={ !showTransferButton }
|
||||
onClick={ this.onTransferClick } />,
|
||||
onClick={ this.onTransferClick } />
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<Button
|
||||
key='delete'
|
||||
icon={ <ActionDelete /> }
|
||||
label='delete'
|
||||
onClick={ this.showDeleteDialog } />,
|
||||
onClick={ this.showDeleteDialog } />
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
<Button
|
||||
key='editmeta'
|
||||
icon={ <ContentCreate /> }
|
||||
label='edit'
|
||||
onClick={ this.onEditClick } />,
|
||||
onClick={ this.onEditClick } />
|
||||
);
|
||||
|
||||
if (owned) {
|
||||
buttons.push(
|
||||
<Button
|
||||
key='settings'
|
||||
icon={ <SettingsIcon /> }
|
||||
label='settings'
|
||||
onClick={ this.onSettingsClick } />
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Actionbar
|
||||
@ -293,12 +307,11 @@ class Wallet extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { wallets, balance, images, address } = this.props;
|
||||
const wallet = wallets[address];
|
||||
const { walletAccount, balance, images } = this.props;
|
||||
|
||||
return (
|
||||
<Transfer
|
||||
account={ wallet }
|
||||
account={ walletAccount }
|
||||
balance={ balance }
|
||||
images={ images }
|
||||
onClose={ this.onTransferClose }
|
||||
@ -342,20 +355,27 @@ function mapStateToProps (_, initProps) {
|
||||
|
||||
return (state) => {
|
||||
const { isTest } = state.nodeStatus;
|
||||
const { wallets } = state.personal;
|
||||
const { accountsInfo = {}, accounts = {} } = state.personal;
|
||||
const { balances } = state.balances;
|
||||
const { images } = state;
|
||||
const walletAccount = accounts[address] || accountsInfo[address] || null;
|
||||
|
||||
if (walletAccount) {
|
||||
walletAccount.address = address;
|
||||
}
|
||||
|
||||
const wallet = state.wallet.wallets[address] || {};
|
||||
const balance = balances[address] || null;
|
||||
const owned = !!accounts[address];
|
||||
|
||||
return {
|
||||
isTest,
|
||||
wallets,
|
||||
address,
|
||||
balance,
|
||||
images,
|
||||
address,
|
||||
wallet
|
||||
isTest,
|
||||
owned,
|
||||
wallet,
|
||||
walletAccount
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Run with `webpack --config webpack.libraries.js --progress`
|
||||
// Run with `webpack --config webpack.libraries.js`
|
||||
|
||||
const path = require('path');
|
||||
|
||||
@ -38,6 +38,13 @@ module.exports = {
|
||||
library: '[name].js',
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': path.resolve(__dirname, '../src')
|
||||
}
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
@ -69,6 +69,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': path.resolve(__dirname, '../src')
|
||||
},
|
||||
modules: [
|
||||
path.resolve('./src'),
|
||||
path.join(__dirname, '../node_modules')
|
||||
|
@ -24,6 +24,7 @@ const postcssNested = require('postcss-nested');
|
||||
const postcssVars = require('postcss-simple-vars');
|
||||
const rucksack = require('rucksack-css');
|
||||
const CircularDependencyPlugin = require('circular-dependency-plugin');
|
||||
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
|
||||
|
||||
const ENV = process.env.NODE_ENV || 'development';
|
||||
const isProd = ENV === 'production';
|
||||
@ -79,6 +80,10 @@ function getPlugins (_isProd = isProd) {
|
||||
];
|
||||
|
||||
const plugins = [
|
||||
new ProgressBarPlugin({
|
||||
format: '[:msg] [:bar] ' + ':percent' + ' (:elapsed seconds)'
|
||||
}),
|
||||
|
||||
// NB: HappyPack is not yet working with Webpack 2... (as of Nov. 26)
|
||||
|
||||
// new HappyPack({
|
||||
|
@ -64,6 +64,13 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': path.resolve(__dirname, '../src')
|
||||
}
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, '../', `${DEST}/`),
|
||||
|
@ -27,7 +27,7 @@ use user_defaults::UserDefaults;
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum SpecType {
|
||||
Mainnet,
|
||||
Testnet,
|
||||
Morden,
|
||||
Ropsten,
|
||||
Olympic,
|
||||
Classic,
|
||||
@ -49,8 +49,8 @@ impl str::FromStr for SpecType {
|
||||
let spec = match s {
|
||||
"frontier" | "homestead" | "mainnet" => SpecType::Mainnet,
|
||||
"frontier-dogmatic" | "homestead-dogmatic" | "classic" => SpecType::Classic,
|
||||
"morden" | "testnet" | "classic-testnet" => SpecType::Testnet,
|
||||
"ropsten" => SpecType::Ropsten,
|
||||
"morden" | "classic-testnet" => SpecType::Morden,
|
||||
"ropsten" | "testnet" => SpecType::Ropsten,
|
||||
"olympic" => SpecType::Olympic,
|
||||
"expanse" => SpecType::Expanse,
|
||||
"dev" => SpecType::Dev,
|
||||
@ -64,7 +64,7 @@ impl SpecType {
|
||||
pub fn spec(&self) -> Result<Spec, String> {
|
||||
match *self {
|
||||
SpecType::Mainnet => Ok(ethereum::new_frontier()),
|
||||
SpecType::Testnet => Ok(ethereum::new_morden()),
|
||||
SpecType::Morden => Ok(ethereum::new_morden()),
|
||||
SpecType::Ropsten => Ok(ethereum::new_ropsten()),
|
||||
SpecType::Olympic => Ok(ethereum::new_olympic()),
|
||||
SpecType::Classic => Ok(ethereum::new_classic()),
|
||||
@ -292,12 +292,12 @@ mod tests {
|
||||
assert_eq!(SpecType::Mainnet, "frontier".parse().unwrap());
|
||||
assert_eq!(SpecType::Mainnet, "homestead".parse().unwrap());
|
||||
assert_eq!(SpecType::Mainnet, "mainnet".parse().unwrap());
|
||||
assert_eq!(SpecType::Testnet, "testnet".parse().unwrap());
|
||||
assert_eq!(SpecType::Testnet, "morden".parse().unwrap());
|
||||
assert_eq!(SpecType::Ropsten, "testnet".parse().unwrap());
|
||||
assert_eq!(SpecType::Morden, "morden".parse().unwrap());
|
||||
assert_eq!(SpecType::Ropsten, "ropsten".parse().unwrap());
|
||||
assert_eq!(SpecType::Olympic, "olympic".parse().unwrap());
|
||||
assert_eq!(SpecType::Classic, "classic".parse().unwrap());
|
||||
assert_eq!(SpecType::Testnet, "classic-testnet".parse().unwrap());
|
||||
assert_eq!(SpecType::Morden, "classic-testnet".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -193,6 +193,7 @@ fn rpc_eth_logs() {
|
||||
data: vec![1,2,3],
|
||||
},
|
||||
transaction_index: 0,
|
||||
transaction_log_index: 0,
|
||||
transaction_hash: H256::default(),
|
||||
log_index: 0,
|
||||
}, LocalizedLogEntry {
|
||||
@ -204,8 +205,9 @@ fn rpc_eth_logs() {
|
||||
data: vec![1,2,3],
|
||||
},
|
||||
transaction_index: 0,
|
||||
transaction_log_index: 1,
|
||||
transaction_hash: H256::default(),
|
||||
log_index: 0,
|
||||
log_index: 1,
|
||||
}]);
|
||||
|
||||
|
||||
@ -213,8 +215,8 @@ fn rpc_eth_logs() {
|
||||
let request2 = r#"{"jsonrpc": "2.0", "method": "eth_getLogs", "params": [{"limit":1}], "id": 1}"#;
|
||||
let request3 = r#"{"jsonrpc": "2.0", "method": "eth_getLogs", "params": [{"limit":0}], "id": 1}"#;
|
||||
|
||||
let response1 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"},{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"id":1}"#;
|
||||
let response2 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"id":1}"#;
|
||||
let response1 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x0","type":"mined"},{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x1","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x1","type":"mined"}],"id":1}"#;
|
||||
let response2 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x1","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x1","type":"mined"}],"id":1}"#;
|
||||
let response3 = r#"{"jsonrpc":"2.0","result":[],"id":1}"#;
|
||||
|
||||
assert_eq!(tester.io.handle_request_sync(request1), Some(response1.to_owned()));
|
||||
@ -235,6 +237,7 @@ fn rpc_logs_filter() {
|
||||
data: vec![1,2,3],
|
||||
},
|
||||
transaction_index: 0,
|
||||
transaction_log_index: 0,
|
||||
transaction_hash: H256::default(),
|
||||
log_index: 0,
|
||||
}, LocalizedLogEntry {
|
||||
@ -246,8 +249,9 @@ fn rpc_logs_filter() {
|
||||
data: vec![1,2,3],
|
||||
},
|
||||
transaction_index: 0,
|
||||
transaction_log_index: 1,
|
||||
transaction_hash: H256::default(),
|
||||
log_index: 0,
|
||||
log_index: 1,
|
||||
}]);
|
||||
|
||||
// Register filters first
|
||||
@ -261,8 +265,8 @@ fn rpc_logs_filter() {
|
||||
|
||||
let request_changes1 = r#"{"jsonrpc": "2.0", "method": "eth_getFilterChanges", "params": ["0x0"], "id": 1}"#;
|
||||
let request_changes2 = r#"{"jsonrpc": "2.0", "method": "eth_getFilterChanges", "params": ["0x1"], "id": 1}"#;
|
||||
let response1 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"},{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"id":1}"#;
|
||||
let response2 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"id":1}"#;
|
||||
let response1 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x0","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x0","type":"mined"},{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x1","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x1","type":"mined"}],"id":1}"#;
|
||||
let response2 = r#"{"jsonrpc":"2.0","result":[{"address":"0x0000000000000000000000000000000000000000","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","data":"0x010203","logIndex":"0x1","topics":[],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x1","type":"mined"}],"id":1}"#;
|
||||
|
||||
assert_eq!(tester.io.handle_request_sync(request_changes1), Some(response1.to_owned()));
|
||||
assert_eq!(tester.io.handle_request_sync(request_changes2), Some(response2.to_owned()));
|
||||
@ -951,6 +955,7 @@ fn rpc_eth_transaction_receipt() {
|
||||
block_number: 0x4510c,
|
||||
transaction_hash: H256::new(),
|
||||
transaction_index: 0,
|
||||
transaction_log_index: 0,
|
||||
log_index: 1,
|
||||
}],
|
||||
log_bloom: 0.into(),
|
||||
@ -967,7 +972,7 @@ fn rpc_eth_transaction_receipt() {
|
||||
"params": ["0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"],
|
||||
"id": 1
|
||||
}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","contractAddress":null,"cumulativeGasUsed":"0x20","gasUsed":"0x10","logs":[{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","data":"0x","logIndex":"0x1","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","type":"mined"}],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","root":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0"},"id":1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","contractAddress":null,"cumulativeGasUsed":"0x20","gasUsed":"0x10","logs":[{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","data":"0x","logIndex":"0x1","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","transactionLogIndex":"0x0","type":"mined"}],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","root":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0"},"id":1}"#;
|
||||
|
||||
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
|
||||
}
|
||||
|
@ -38,9 +38,12 @@ pub struct Log {
|
||||
/// Transaction Index
|
||||
#[serde(rename="transactionIndex")]
|
||||
pub transaction_index: Option<U256>,
|
||||
/// Log Index
|
||||
/// Log Index in Block
|
||||
#[serde(rename="logIndex")]
|
||||
pub log_index: Option<U256>,
|
||||
/// Log Index in Transaction
|
||||
#[serde(rename="transactionLogIndex")]
|
||||
pub transaction_log_index: Option<U256>,
|
||||
/// Log Type
|
||||
#[serde(rename="type")]
|
||||
pub log_type: String,
|
||||
@ -57,6 +60,7 @@ impl From<LocalizedLogEntry> for Log {
|
||||
transaction_hash: Some(e.transaction_hash.into()),
|
||||
transaction_index: Some(e.transaction_index.into()),
|
||||
log_index: Some(e.log_index.into()),
|
||||
transaction_log_index: Some(e.transaction_log_index.into()),
|
||||
log_type: "mined".to_owned(),
|
||||
}
|
||||
}
|
||||
@ -73,6 +77,7 @@ impl From<LogEntry> for Log {
|
||||
transaction_hash: None,
|
||||
transaction_index: None,
|
||||
log_index: None,
|
||||
transaction_log_index: None,
|
||||
log_type: "pending".to_owned(),
|
||||
}
|
||||
}
|
||||
@ -86,7 +91,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn log_serialization() {
|
||||
let s = r#"{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"data":"0x","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","logIndex":"0x1","type":"mined"}"#;
|
||||
let s = r#"{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"data":"0x","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","logIndex":"0x1","transactionLogIndex":"0x1","type":"mined"}"#;
|
||||
|
||||
let log = Log {
|
||||
address: H160::from_str("33990122638b9132ca29c723bdf037f1a891a70c").unwrap(),
|
||||
@ -99,6 +104,7 @@ mod tests {
|
||||
block_number: Some(U256::from(0x4510c)),
|
||||
transaction_hash: Some(H256::default()),
|
||||
transaction_index: Some(U256::default()),
|
||||
transaction_log_index: Some(1.into()),
|
||||
log_index: Some(U256::from(1)),
|
||||
log_type: "mined".to_owned(),
|
||||
};
|
||||
|
@ -109,7 +109,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn receipt_serialization() {
|
||||
let s = r#"{"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","cumulativeGasUsed":"0x20","gasUsed":"0x10","contractAddress":null,"logs":[{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"data":"0x","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","logIndex":"0x1","type":"mined"}],"root":"0x000000000000000000000000000000000000000000000000000000000000000a","logsBloom":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f"}"#;
|
||||
let s = r#"{"transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","cumulativeGasUsed":"0x20","gasUsed":"0x10","contractAddress":null,"logs":[{"address":"0x33990122638b9132ca29c723bdf037f1a891a70c","topics":["0xa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc","0x4861736852656700000000000000000000000000000000000000000000000000"],"data":"0x","blockHash":"0xed76641c68a1c641aee09a94b3b471f4dc0316efe5ac19cf488e2674cf8d05b5","blockNumber":"0x4510c","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionIndex":"0x0","logIndex":"0x1","transactionLogIndex":null,"type":"mined"}],"root":"0x000000000000000000000000000000000000000000000000000000000000000a","logsBloom":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f"}"#;
|
||||
|
||||
let receipt = Receipt {
|
||||
transaction_hash: Some(0.into()),
|
||||
@ -130,6 +130,7 @@ mod tests {
|
||||
block_number: Some(0x4510c.into()),
|
||||
transaction_hash: Some(0.into()),
|
||||
transaction_index: Some(0.into()),
|
||||
transaction_log_index: None,
|
||||
log_index: Some(1.into()),
|
||||
log_type: "mined".into(),
|
||||
}],
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -22,8 +22,6 @@ use hashdb::{HashDB, DBValue};
|
||||
|
||||
/// Export the standardmap module.
|
||||
pub mod standardmap;
|
||||
/// Export the journal module.
|
||||
pub mod journal;
|
||||
/// Export the node module.
|
||||
pub mod node;
|
||||
/// Export the triedb module.
|
||||
|
@ -18,7 +18,6 @@ use elastic_array::ElasticArray36;
|
||||
use nibbleslice::*;
|
||||
use bytes::*;
|
||||
use rlp::*;
|
||||
use super::journal::*;
|
||||
use hashdb::DBValue;
|
||||
|
||||
/// Partial node key type.
|
||||
@ -123,44 +122,4 @@ impl Node {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the node, adding it to `journal` if necessary and return the RLP valid for
|
||||
/// insertion into a parent node.
|
||||
pub fn encoded_and_added(&self, journal: &mut Journal) -> DBValue {
|
||||
let mut stream = RlpStream::new();
|
||||
match *self {
|
||||
Node::Leaf(ref slice, ref value) => {
|
||||
stream.begin_list(2);
|
||||
stream.append(&&**slice);
|
||||
stream.append(&&**value);
|
||||
},
|
||||
Node::Extension(ref slice, ref raw_rlp) => {
|
||||
stream.begin_list(2);
|
||||
stream.append(&&**slice);
|
||||
stream.append_raw(&&**raw_rlp, 1);
|
||||
},
|
||||
Node::Branch(ref nodes, ref value) => {
|
||||
stream.begin_list(17);
|
||||
for i in 0..16 {
|
||||
stream.append_raw(&*nodes[i], 1);
|
||||
}
|
||||
match *value {
|
||||
Some(ref n) => { stream.append(&&**n); },
|
||||
None => { stream.append_empty_data(); },
|
||||
}
|
||||
},
|
||||
Node::Empty => {
|
||||
stream.append_empty_data();
|
||||
}
|
||||
}
|
||||
let node = DBValue::from_slice(stream.as_raw());
|
||||
match node.len() {
|
||||
0 ... 31 => node,
|
||||
_ => {
|
||||
let mut stream = RlpStream::new();
|
||||
journal.new_node(node, &mut stream);
|
||||
DBValue::from_slice(stream.as_raw())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user