Merge branch 'master' into auth-bft

This commit is contained in:
keorn 2016-12-07 14:50:17 +01:00
commit e9743a3a75
59 changed files with 1037 additions and 383 deletions

3
Cargo.lock generated
View File

@ -298,7 +298,6 @@ dependencies = [
"ethcore-ipc 1.4.0", "ethcore-ipc 1.4.0",
"ethcore-ipc-codegen 1.4.0", "ethcore-ipc-codegen 1.4.0",
"ethcore-ipc-nano 1.4.0", "ethcore-ipc-nano 1.4.0",
"ethcore-network 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"ethjson 0.1.0", "ethjson 0.1.0",
"ethkey 0.2.0", "ethkey 0.2.0",
@ -1272,7 +1271,7 @@ dependencies = [
[[package]] [[package]]
name = "parity-ui-precompiled" name = "parity-ui-precompiled"
version = "1.4.0" version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#57f5bf943f24cf761ba58c1bea35a845e0b12414" source = "git+https://github.com/ethcore/js-precompiled.git#c3e0b5772f508d9261e8b10141edcad2e8212005"
dependencies = [ dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

View File

@ -304,6 +304,9 @@ impl SignedTransaction {
/// 0 if `v` would have been 27 under "Electrum" notation, 1 if 28 or 4 if invalid. /// 0 if `v` would have been 27 under "Electrum" notation, 1 if 28 or 4 if invalid.
pub fn standard_v(&self) -> u8 { match self.v { v if v == 27 || v == 28 || v > 36 => ((v - 1) % 2) as u8, _ => 4 } } pub fn standard_v(&self) -> u8 { match self.v { v if v == 27 || v == 28 || v > 36 => ((v - 1) % 2) as u8, _ => 4 } }
/// The `v` value that appears in the RLP.
pub fn original_v(&self) -> u64 { self.v }
/// The network ID, or `None` if this is a global transaction. /// The network ID, or `None` if this is a global transaction.
pub fn network_id(&self) -> Option<u64> { pub fn network_id(&self) -> Option<u64> {
match self.v { match self.v {

View File

@ -53,6 +53,8 @@ pub struct Config {
/// Maximum heap memory to use. /// Maximum heap memory to use.
/// When the limit is reached, is_full returns true. /// When the limit is reached, is_full returns true.
pub max_mem_use: usize, pub max_mem_use: usize,
/// Settings for the number of verifiers and adaptation strategy.
pub verifier_settings: VerifierSettings,
} }
impl Default for Config { impl Default for Config {
@ -60,6 +62,26 @@ impl Default for Config {
Config { Config {
max_queue_size: 30000, max_queue_size: 30000,
max_mem_use: 50 * 1024 * 1024, max_mem_use: 50 * 1024 * 1024,
verifier_settings: VerifierSettings::default(),
}
}
}
/// Verifier settings.
#[derive(Debug, PartialEq, Clone)]
pub struct VerifierSettings {
/// Whether to scale amount of verifiers according to load.
// Todo: replace w/ strategy enum?
pub scale_verifiers: bool,
/// Beginning amount of verifiers.
pub num_verifiers: usize,
}
impl Default for VerifierSettings {
fn default() -> Self {
VerifierSettings {
scale_verifiers: false,
num_verifiers: MAX_VERIFIERS,
} }
} }
} }
@ -114,6 +136,7 @@ pub struct VerificationQueue<K: Kind> {
ticks_since_adjustment: AtomicUsize, ticks_since_adjustment: AtomicUsize,
max_queue_size: usize, max_queue_size: usize,
max_mem_use: usize, max_mem_use: usize,
scale_verifiers: bool,
verifier_handles: Vec<JoinHandle<()>>, verifier_handles: Vec<JoinHandle<()>>,
state: Arc<(Mutex<State>, Condvar)>, state: Arc<(Mutex<State>, Condvar)>,
} }
@ -198,13 +221,16 @@ impl<K: Kind> VerificationQueue<K> {
}); });
let empty = Arc::new(SCondvar::new()); let empty = Arc::new(SCondvar::new());
let panic_handler = PanicHandler::new_in_arc(); let panic_handler = PanicHandler::new_in_arc();
let scale_verifiers = config.verifier_settings.scale_verifiers;
let max_verifiers = min(::num_cpus::get(), MAX_VERIFIERS); let num_cpus = ::num_cpus::get();
let default_amount = max(::num_cpus::get(), 3) - 2; let max_verifiers = min(num_cpus, MAX_VERIFIERS);
let default_amount = max(1, min(max_verifiers, config.verifier_settings.num_verifiers));
let state = Arc::new((Mutex::new(State::Work(default_amount)), Condvar::new())); let state = Arc::new((Mutex::new(State::Work(default_amount)), Condvar::new()));
let mut verifier_handles = Vec::with_capacity(max_verifiers); let mut verifier_handles = Vec::with_capacity(max_verifiers);
debug!(target: "verification", "Allocating {} verifiers, {} initially active", max_verifiers, default_amount); debug!(target: "verification", "Allocating {} verifiers, {} initially active", max_verifiers, default_amount);
debug!(target: "verification", "Verifier auto-scaling {}", if scale_verifiers { "enabled" } else { "disabled" });
for i in 0..max_verifiers { for i in 0..max_verifiers {
debug!(target: "verification", "Adding verification thread #{}", i); debug!(target: "verification", "Adding verification thread #{}", i);
@ -248,6 +274,7 @@ impl<K: Kind> VerificationQueue<K> {
ticks_since_adjustment: AtomicUsize::new(0), ticks_since_adjustment: AtomicUsize::new(0),
max_queue_size: max(config.max_queue_size, MIN_QUEUE_LIMIT), max_queue_size: max(config.max_queue_size, MIN_QUEUE_LIMIT),
max_mem_use: max(config.max_mem_use, MIN_MEM_LIMIT), max_mem_use: max(config.max_mem_use, MIN_MEM_LIMIT),
scale_verifiers: scale_verifiers,
verifier_handles: verifier_handles, verifier_handles: verifier_handles,
state: state, state: state,
} }
@ -597,6 +624,8 @@ impl<K: Kind> VerificationQueue<K> {
self.processing.write().shrink_to_fit(); self.processing.write().shrink_to_fit();
if !self.scale_verifiers { return }
if self.ticks_since_adjustment.fetch_add(1, AtomicOrdering::SeqCst) + 1 >= READJUSTMENT_PERIOD { if self.ticks_since_adjustment.fetch_add(1, AtomicOrdering::SeqCst) + 1 >= READJUSTMENT_PERIOD {
self.ticks_since_adjustment.store(0, AtomicOrdering::SeqCst); self.ticks_since_adjustment.store(0, AtomicOrdering::SeqCst);
} else { } else {
@ -675,10 +704,15 @@ mod tests {
use error::*; use error::*;
use views::*; use views::*;
fn get_test_queue() -> BlockQueue { // create a test block queue.
// auto_scaling enables verifier adjustment.
fn get_test_queue(auto_scale: bool) -> BlockQueue {
let spec = get_test_spec(); let spec = get_test_spec();
let engine = spec.engine; let engine = spec.engine;
BlockQueue::new(Config::default(), engine, IoChannel::disconnected(), true)
let mut config = Config::default();
config.verifier_settings.scale_verifiers = auto_scale;
BlockQueue::new(config, engine, IoChannel::disconnected(), true)
} }
#[test] #[test]
@ -691,7 +725,7 @@ mod tests {
#[test] #[test]
fn can_import_blocks() { fn can_import_blocks() {
let queue = get_test_queue(); let queue = get_test_queue(false);
if let Err(e) = queue.import(Unverified::new(get_good_dummy_block())) { if let Err(e) = queue.import(Unverified::new(get_good_dummy_block())) {
panic!("error importing block that is valid by definition({:?})", e); panic!("error importing block that is valid by definition({:?})", e);
} }
@ -699,7 +733,7 @@ mod tests {
#[test] #[test]
fn returns_error_for_duplicates() { fn returns_error_for_duplicates() {
let queue = get_test_queue(); let queue = get_test_queue(false);
if let Err(e) = queue.import(Unverified::new(get_good_dummy_block())) { if let Err(e) = queue.import(Unverified::new(get_good_dummy_block())) {
panic!("error importing block that is valid by definition({:?})", e); panic!("error importing block that is valid by definition({:?})", e);
} }
@ -718,7 +752,7 @@ mod tests {
#[test] #[test]
fn returns_ok_for_drained_duplicates() { fn returns_ok_for_drained_duplicates() {
let queue = get_test_queue(); let queue = get_test_queue(false);
let block = get_good_dummy_block(); let block = get_good_dummy_block();
let hash = BlockView::new(&block).header().hash().clone(); let hash = BlockView::new(&block).header().hash().clone();
if let Err(e) = queue.import(Unverified::new(block)) { if let Err(e) = queue.import(Unverified::new(block)) {
@ -735,7 +769,7 @@ mod tests {
#[test] #[test]
fn returns_empty_once_finished() { fn returns_empty_once_finished() {
let queue = get_test_queue(); let queue = get_test_queue(false);
queue.import(Unverified::new(get_good_dummy_block())) queue.import(Unverified::new(get_good_dummy_block()))
.expect("error importing block that is valid by definition"); .expect("error importing block that is valid by definition");
queue.flush(); queue.flush();
@ -763,7 +797,7 @@ mod tests {
fn scaling_limits() { fn scaling_limits() {
use super::MAX_VERIFIERS; use super::MAX_VERIFIERS;
let queue = get_test_queue(); let queue = get_test_queue(true);
queue.scale_verifiers(MAX_VERIFIERS + 1); queue.scale_verifiers(MAX_VERIFIERS + 1);
assert!(queue.num_verifiers() < MAX_VERIFIERS + 1); assert!(queue.num_verifiers() < MAX_VERIFIERS + 1);
@ -775,7 +809,7 @@ mod tests {
#[test] #[test]
fn readjust_verifiers() { fn readjust_verifiers() {
let queue = get_test_queue(); let queue = get_test_queue(true);
// put all the verifiers to sleep to ensure // put all the verifiers to sleep to ensure
// the test isn't timing sensitive. // the test isn't timing sensitive.

View File

@ -1,6 +1,6 @@
{ {
"name": "parity.js", "name": "parity.js",
"version": "0.2.94", "version": "0.2.96",
"main": "release/index.js", "main": "release/index.js",
"jsnext:main": "src/index.js", "jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>", "author": "Parity Team <admin@parity.io>",

View File

@ -189,15 +189,21 @@ export default class Contract {
}); });
} }
_encodeOptions (func, options, values) { getCallData = (func, options, values) => {
let data = options.data;
const tokens = func ? this._abi.encodeTokens(func.inputParamTypes(), values) : null; const tokens = func ? this._abi.encodeTokens(func.inputParamTypes(), values) : null;
const call = tokens ? func.encodeCall(tokens) : null; const call = tokens ? func.encodeCall(tokens) : null;
if (options.data && options.data.substr(0, 2) === '0x') { if (data && data.substr(0, 2) === '0x') {
options.data = options.data.substr(2); data = data.substr(2);
} }
options.data = `0x${options.data || ''}${call || ''}`;
return `0x${data || ''}${call || ''}`;
}
_encodeOptions (func, options, values) {
options.data = this.getCallData(func, options, values);
return options; return options;
} }
@ -209,10 +215,10 @@ export default class Contract {
_bindFunction = (func) => { _bindFunction = (func) => {
func.call = (options, values = []) => { func.call = (options, values = []) => {
const callData = this._encodeOptions(func, this._addOptionsTo(options), values); const callParams = this._encodeOptions(func, this._addOptionsTo(options), values);
return this._api.eth return this._api.eth
.call(callData) .call(callParams)
.then((encoded) => func.decodeOutput(encoded)) .then((encoded) => func.decodeOutput(encoded))
.then((tokens) => tokens.map((token) => token.value)) .then((tokens) => tokens.map((token) => token.value))
.then((returns) => returns.length === 1 ? returns[0] : returns); .then((returns) => returns.length === 1 ? returns[0] : returns);

View File

@ -1,3 +1,19 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Card, CardHeader, CardText } from 'material-ui/Card'; import { Card, CardHeader, CardText } from 'material-ui/Card';
import TextField from 'material-ui/TextField'; import TextField from 'material-ui/TextField';

View File

@ -16,18 +16,62 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Form, TypedInput, Input, AddressSelect } from '../../../ui'; import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui';
import { parseAbiType } from '../../../util/abi'; import { parseAbiType } from '~/util/abi';
import styles from '../createWallet.css';
export default class WalletDetails extends Component { export default class WalletDetails extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired, wallet: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired, errors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired,
walletType: PropTypes.string.isRequired
}; };
render () { render () {
const { walletType } = this.props;
if (walletType === 'WATCH') {
return this.renderWatchDetails();
}
return this.renderMultisigDetails();
}
renderWatchDetails () {
const { wallet, errors } = this.props;
return (
<Form>
<InputAddress
label='wallet address'
hint='the wallet contract address'
value={ wallet.address }
error={ errors.address }
onChange={ this.onAddressChange }
/>
<Input
label='wallet name'
hint='the local name for this wallet'
value={ wallet.name }
error={ errors.name }
onChange={ this.onNameChange }
/>
<Input
label='wallet description (optional)'
hint='the local description for this wallet'
value={ wallet.description }
onChange={ this.onDescriptionChange }
/>
</Form>
);
}
renderMultisigDetails () {
const { accounts, wallet, errors } = this.props; const { accounts, wallet, errors } = this.props;
return ( return (
@ -64,27 +108,34 @@ export default class WalletDetails extends Component {
param={ parseAbiType('address[]') } param={ parseAbiType('address[]') }
/> />
<TypedInput <div className={ styles.splitInput }>
label='required owners' <TypedInput
hint='number of required owners to accept a transaction' label='required owners'
value={ wallet.required } hint='number of required owners to accept a transaction'
error={ errors.required } value={ wallet.required }
onChange={ this.onRequiredChange } error={ errors.required }
param={ parseAbiType('uint') } onChange={ this.onRequiredChange }
/> param={ parseAbiType('uint') }
min={ 1 }
/>
<TypedInput <TypedInput
label='wallet day limit' label='wallet day limit'
hint='number of days to wait for other owners confirmation' hint='number of days to wait for other owners confirmation'
value={ wallet.daylimit } value={ wallet.daylimit }
error={ errors.daylimit } error={ errors.daylimit }
onChange={ this.onDaylimitChange } onChange={ this.onDaylimitChange }
param={ parseAbiType('uint') } param={ parseAbiType('uint') }
/> />
</div>
</Form> </Form>
); );
} }
onAddressChange = (_, address) => {
this.props.onChange({ address });
}
onAccoutChange = (_, account) => { onAccoutChange = (_, account) => {
this.props.onChange({ account }); this.props.onChange({ account });
} }

View File

@ -16,7 +16,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { CompletedStep, IdentityIcon, CopyToClipboard } from '../../../ui'; import { CompletedStep, IdentityIcon, CopyToClipboard } from '~/ui';
import styles from '../createWallet.css'; import styles from '../createWallet.css';
@ -34,15 +34,21 @@ export default class WalletInfo extends Component {
daylimit: PropTypes.oneOfType([ daylimit: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.number PropTypes.number
]).isRequired ]).isRequired,
deployed: PropTypes.bool
}; };
render () { render () {
const { address, required, daylimit, name } = this.props; const { address, required, daylimit, name, deployed } = this.props;
return ( return (
<CompletedStep> <CompletedStep>
<div><code>{ name }</code> has been deployed at</div> <div>
<code>{ name }</code>
<span> has been </span>
<span> { deployed ? 'deployed' : 'added' } at </span>
</div>
<div> <div>
<CopyToClipboard data={ address } label='copy address to clipboard' /> <CopyToClipboard data={ address } label='copy address to clipboard' />
<IdentityIcon address={ address } inline center className={ styles.identityicon } /> <IdentityIcon address={ address } inline center className={ styles.identityicon } />
@ -63,9 +69,9 @@ export default class WalletInfo extends Component {
} }
renderOwners () { renderOwners () {
const { account, owners } = this.props; const { account, owners, deployed } = this.props;
return [].concat(account, owners).map((address, id) => ( return [].concat(deployed ? account : null, owners).filter((a) => a).map((address, id) => (
<div key={ id } className={ styles.owner }> <div key={ id } className={ styles.owner }>
<IdentityIcon address={ address } inline center className={ styles.identityicon } /> <IdentityIcon address={ address } inline center className={ styles.identityicon } />
<div className={ styles.address }>{ this.addressToString(address) }</div> <div className={ styles.address }>{ this.addressToString(address) }</div>

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './walletType.js';

View File

@ -0,0 +1,58 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { RadioButtons } from '~/ui';
// import styles from '../createWallet.css';
export default class WalletType extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
type: PropTypes.string.isRequired
};
render () {
const { type } = this.props;
return (
<RadioButtons
name='contractType'
value={ type }
values={ this.getTypes() }
onChange={ this.onTypeChange }
/>
);
}
getTypes () {
return [
{
label: 'Multi-Sig wallet', key: 'MULTISIG',
description: 'A standard multi-signature Wallet'
},
{
label: 'Watch a wallet', key: 'WATCH',
description: 'Add an existing wallet to your accounts'
}
];
}
onTypeChange = (type) => {
this.props.onChange(type.key);
}
}

View File

@ -37,3 +37,22 @@
height: 24px; height: 24px;
} }
} }
.splitInput {
display: flex;
flex-direction: row;
> * {
flex: 1;
margin: 0 0.25em;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}

View File

@ -21,8 +21,9 @@ import ActionDone from 'material-ui/svg-icons/action/done';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { Button, Modal, TxHash, BusyStep } from '../../ui'; import { Button, Modal, TxHash, BusyStep } from '~/ui';
import WalletType from './WalletType';
import WalletDetails from './WalletDetails'; import WalletDetails from './WalletDetails';
import WalletInfo from './WalletInfo'; import WalletInfo from './WalletInfo';
import CreateWalletStore from './createWalletStore'; import CreateWalletStore from './createWalletStore';
@ -64,7 +65,7 @@ export default class CreateWallet extends Component {
visible visible
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
current={ stage } current={ stage }
steps={ steps } steps={ steps.map((s) => s.title) }
waiting={ waiting } waiting={ waiting }
> >
{ this.renderPage() } { this.renderPage() }
@ -98,24 +99,35 @@ export default class CreateWallet extends Component {
required={ this.store.wallet.required } required={ this.store.wallet.required }
daylimit={ this.store.wallet.daylimit } daylimit={ this.store.wallet.daylimit }
name={ this.store.wallet.name } name={ this.store.wallet.name }
deployed={ this.store.deployed }
/> />
); );
default:
case 'DETAILS': case 'DETAILS':
return ( return (
<WalletDetails <WalletDetails
accounts={ accounts } accounts={ accounts }
wallet={ this.store.wallet } wallet={ this.store.wallet }
errors={ this.store.errors } errors={ this.store.errors }
walletType={ this.store.walletType }
onChange={ this.store.onChange } onChange={ this.store.onChange }
/> />
); );
default:
case 'TYPE':
return (
<WalletType
onChange={ this.store.onTypeChange }
type={ this.store.walletType }
/>
);
} }
} }
renderDialogActions () { renderDialogActions () {
const { step, hasErrors, rejected, onCreate } = this.store; const { step, hasErrors, rejected, onCreate, onNext, onAdd } = this.store;
const cancelBtn = ( const cancelBtn = (
<Button <Button
@ -149,12 +161,11 @@ export default class CreateWallet extends Component {
/> />
); );
const createBtn = ( const nextBtn = (
<Button <Button
icon={ <NavigationArrowForward /> } icon={ <NavigationArrowForward /> }
label='Create' label='Next'
disabled={ hasErrors } onClick={ onNext }
onClick={ onCreate }
/> />
); );
@ -169,9 +180,30 @@ export default class CreateWallet extends Component {
case 'INFO': case 'INFO':
return [ doneBtn ]; return [ doneBtn ];
default:
case 'DETAILS': case 'DETAILS':
return [ cancelBtn, createBtn ]; if (this.store.walletType === 'WATCH') {
return [ cancelBtn, (
<Button
icon={ <NavigationArrowForward /> }
label='Add'
disabled={ hasErrors }
onClick={ onAdd }
/>
) ];
}
return [ cancelBtn, (
<Button
icon={ <NavigationArrowForward /> }
label='Create'
disabled={ hasErrors }
onClick={ onCreate }
/>
) ];
default:
case 'TYPE':
return [ cancelBtn, nextBtn ];
} }
} }

View File

@ -16,26 +16,29 @@
import { observable, computed, action, transaction } from 'mobx'; import { observable, computed, action, transaction } from 'mobx';
import { ERRORS, validateUint, validateAddress, validateName } from '../../util/validation'; import { validateUint, validateAddress, validateName } from '../../util/validation';
import { ERROR_CODES } from '../../api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import { wallet as walletAbi } from '../../contracts/abi'; import Contract from '~/api/contract';
import { wallet as walletCode } from '../../contracts/code'; import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode } from '~/contracts/code';
import WalletsUtils from '~/util/wallets';
const STEPS = { const STEPS = {
TYPE: { title: 'wallet type' },
DETAILS: { title: 'wallet details' }, DETAILS: { title: 'wallet details' },
DEPLOYMENT: { title: 'wallet deployment', waiting: true }, DEPLOYMENT: { title: 'wallet deployment', waiting: true },
INFO: { title: 'wallet informaton' } INFO: { title: 'wallet informaton' }
}; };
const STEPS_KEYS = Object.keys(STEPS);
export default class CreateWalletStore { export default class CreateWalletStore {
@observable step = null; @observable step = null;
@observable rejected = false; @observable rejected = false;
@observable deployState = null; @observable deployState = null;
@observable deployError = null; @observable deployError = null;
@observable deployed = false;
@observable txhash = null; @observable txhash = null;
@ -49,45 +52,103 @@ export default class CreateWalletStore {
name: '', name: '',
description: '' description: ''
}; };
@observable walletType = 'MULTISIG';
@observable errors = { @observable errors = {
account: null, account: null,
address: null,
owners: null, owners: null,
required: null, required: null,
daylimit: null, daylimit: null,
name: null
name: ERRORS.invalidName
}; };
@computed get stage () { @computed get stage () {
return STEPS_KEYS.findIndex((k) => k === this.step); return this.stepsKeys.findIndex((k) => k === this.step);
} }
@computed get hasErrors () { @computed get hasErrors () {
return !!Object.values(this.errors).find((e) => !!e); return !!Object.keys(this.errors)
.filter((errorKey) => {
if (this.walletType === 'WATCH') {
return ['address', 'name'].includes(errorKey);
}
return errorKey !== 'address';
})
.find((key) => !!this.errors[key]);
} }
steps = Object.values(STEPS).map((s) => s.title); @computed get stepsKeys () {
waiting = Object.values(STEPS) return this.steps.map((s) => s.key);
.map((s, idx) => ({ idx, waiting: s.waiting })) }
.filter((s) => s.waiting)
.map((s) => s.idx); @computed get steps () {
return Object
.keys(STEPS)
.map((key) => {
return {
...STEPS[key],
key
};
})
.filter((step) => {
return (this.walletType !== 'WATCH' || step.key !== 'DEPLOYMENT');
});
}
@computed get waiting () {
this.steps
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
}
constructor (api, accounts) { constructor (api, accounts) {
this.api = api; this.api = api;
this.step = STEPS_KEYS[0]; this.step = this.stepsKeys[0];
this.wallet.account = Object.values(accounts)[0].address; this.wallet.account = Object.values(accounts)[0].address;
this.validateWallet(this.wallet);
}
@action onTypeChange = (type) => {
this.walletType = type;
this.validateWallet(this.wallet);
}
@action onNext = () => {
const stepIndex = this.stepsKeys.findIndex((k) => k === this.step) + 1;
this.step = this.stepsKeys[stepIndex];
} }
@action onChange = (_wallet) => { @action onChange = (_wallet) => {
const newWallet = Object.assign({}, this.wallet, _wallet); const newWallet = Object.assign({}, this.wallet, _wallet);
const { errors, wallet } = this.validateWallet(newWallet); this.validateWallet(newWallet);
}
transaction(() => { @action onAdd = () => {
this.wallet = wallet; if (this.hasErrors) {
this.errors = errors; return;
}); }
const walletContract = new Contract(this.api, walletAbi).at(this.wallet.address);
return Promise
.all([
WalletsUtils.fetchRequire(walletContract),
WalletsUtils.fetchOwners(walletContract),
WalletsUtils.fetchDailylimit(walletContract)
])
.then(([ require, owners, dailylimit ]) => {
transaction(() => {
this.wallet.owners = owners;
this.wallet.required = require.toNumber();
this.wallet.dailylimit = dailylimit.limit;
});
return this.addWallet(this.wallet);
});
} }
@action onCreate = () => { @action onCreate = () => {
@ -97,7 +158,7 @@ export default class CreateWalletStore {
this.step = 'DEPLOYMENT'; this.step = 'DEPLOYMENT';
const { account, owners, required, daylimit, name, description } = this.wallet; const { account, owners, required, daylimit } = this.wallet;
const options = { const options = {
data: walletCode, data: walletCode,
@ -108,24 +169,9 @@ export default class CreateWalletStore {
.newContract(walletAbi) .newContract(walletAbi)
.deploy(options, [ owners, required, daylimit ], this.onDeploymentState) .deploy(options, [ owners, required, daylimit ], this.onDeploymentState)
.then((address) => { .then((address) => {
return Promise this.deployed = true;
.all([ this.wallet.address = address;
this.api.parity.setAccountName(address, name), return this.addWallet(this.wallet);
this.api.parity.setAccountMeta(address, {
abi: walletAbi,
wallet: true,
timestamp: Date.now(),
deleted: false,
description,
name
})
])
.then(() => {
transaction(() => {
this.wallet.address = address;
this.step = 'INFO';
});
});
}) })
.catch((error) => { .catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) { if (error.code === ERROR_CODES.REQUEST_REJECTED) {
@ -138,6 +184,27 @@ export default class CreateWalletStore {
}); });
} }
@action addWallet = (wallet) => {
const { address, name, description } = wallet;
return Promise
.all([
this.api.parity.setAccountName(address, name),
this.api.parity.setAccountMeta(address, {
abi: walletAbi,
wallet: true,
timestamp: Date.now(),
deleted: false,
description,
name,
tags: ['wallet']
})
])
.then(() => {
this.step = 'INFO';
});
}
onDeploymentState = (error, data) => { onDeploymentState = (error, data) => {
if (error) { if (error) {
return console.error('createWallet::onDeploymentState', error); return console.error('createWallet::onDeploymentState', error);
@ -173,13 +240,15 @@ export default class CreateWalletStore {
} }
} }
validateWallet = (_wallet) => { @action validateWallet = (_wallet) => {
const addressValidation = validateAddress(_wallet.address);
const accountValidation = validateAddress(_wallet.account); const accountValidation = validateAddress(_wallet.account);
const requiredValidation = validateUint(_wallet.required); const requiredValidation = validateUint(_wallet.required);
const daylimitValidation = validateUint(_wallet.daylimit); const daylimitValidation = validateUint(_wallet.daylimit);
const nameValidation = validateName(_wallet.name); const nameValidation = validateName(_wallet.name);
const errors = { const errors = {
address: addressValidation.addressError,
account: accountValidation.addressError, account: accountValidation.addressError,
required: requiredValidation.valueError, required: requiredValidation.valueError,
daylimit: daylimitValidation.valueError, daylimit: daylimitValidation.valueError,
@ -188,12 +257,16 @@ export default class CreateWalletStore {
const wallet = { const wallet = {
..._wallet, ..._wallet,
address: addressValidation.address,
account: accountValidation.address, account: accountValidation.address,
required: requiredValidation.value, required: requiredValidation.value,
daylimit: daylimitValidation.value, daylimit: daylimitValidation.value,
name: nameValidation.name name: nameValidation.name
}; };
return { errors, wallet }; transaction(() => {
this.wallet = wallet;
this.errors = errors;
});
} }
} }

View File

@ -19,7 +19,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { ConfirmDialog, IdentityIcon, IdentityName, Input } from '~/ui'; import { ConfirmDialog, IdentityIcon, IdentityName, Input } from '~/ui';
import { newError } from '../../redux/actions'; import { newError } from '~/redux/actions';
import styles from './deleteAccount.css'; import styles from './deleteAccount.css';

View File

@ -239,11 +239,7 @@ export default class Details extends Component {
} }
renderTokenSelect () { renderTokenSelect () {
const { balance, images, tag, wallet } = this.props; const { balance, images, tag } = this.props;
if (wallet) {
return null;
}
return ( return (
<TokenSelect <TokenSelect

View File

@ -16,7 +16,11 @@
import { observable, computed, action, transaction } from 'mobx'; import { observable, computed, action, transaction } from 'mobx';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { uniq } from 'lodash';
import { wallet as walletAbi } from '~/contracts/abi';
import { bytesToHex } from '~/api/util/format';
import Contract from '~/api/contract';
import ERRORS from './errors'; import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '../../util/constants'; import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '../../util/constants';
@ -71,6 +75,9 @@ export default class TransferStore {
gasLimit = null; gasLimit = null;
onClose = null; onClose = null;
senders = null;
sendersBalances = null;
isWallet = false; isWallet = false;
wallet = null; wallet = null;
@ -108,19 +115,23 @@ export default class TransferStore {
constructor (api, props) { constructor (api, props) {
this.api = api; this.api = api;
const { account, balance, gasLimit, senders, onClose } = props; const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props;
this.account = account; this.account = account;
this.balance = balance; this.balance = balance;
this.gasLimit = gasLimit; this.gasLimit = gasLimit;
this.onClose = onClose; this.onClose = onClose;
this.isWallet = account && account.wallet; this.isWallet = account && account.wallet;
this.newError = newError;
if (this.isWallet) { if (this.isWallet) {
this.wallet = props.wallet; this.wallet = props.wallet;
this.walletContract = new Contract(this.api, walletAbi);
} }
if (senders) { if (senders) {
this.senders = senders;
this.sendersBalances = sendersBalances;
this.senderError = ERRORS.requireSender; this.senderError = ERRORS.requireSender;
} }
} }
@ -217,6 +228,10 @@ export default class TransferStore {
this.txhash = txhash; this.txhash = txhash;
this.busyState = 'Your transaction has been posted to the network'; this.busyState = 'Your transaction has been posted to the network';
}); });
if (this.isWallet) {
return this._attachWalletOperation(txhash);
}
}) })
.catch((error) => { .catch((error) => {
this.sending = false; this.sending = false;
@ -224,6 +239,34 @@ export default class TransferStore {
}); });
} }
@action _attachWalletOperation = (txhash) => {
let ethSubscriptionId = null;
return this.api.subscribe('eth_blockNumber', () => {
this.api.eth
.getTransactionReceipt(txhash)
.then((tx) => {
if (!tx) {
return;
}
const logs = this.walletContract.parseEventLogs(tx.logs);
const operations = uniq(logs
.filter((log) => log && log.params && log.params.operation)
.map((log) => bytesToHex(log.params.operation.value)));
if (operations.length > 0) {
this.operation = operations[0];
}
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
});
}).then((subId) => {
ethSubscriptionId = subId;
});
}
@action _onUpdateAll = (valueAll) => { @action _onUpdateAll = (valueAll) => {
this.valueAll = valueAll; this.valueAll = valueAll;
this.recalculateGas(); this.recalculateGas();
@ -355,19 +398,29 @@ export default class TransferStore {
} }
@action recalculate = () => { @action recalculate = () => {
const { account, balance } = this; const { account } = this;
if (!account || !balance) { if (!account || !this.balance) {
return;
}
const balance = this.senders
? this.sendersBalances[this.sender]
: this.balance;
if (!balance) {
return; return;
} }
const { gas, gasPrice, tag, valueAll, isEth } = this; const { gas, gasPrice, tag, valueAll, isEth } = this;
const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0)); const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0));
const balance_ = balance.tokens.find((b) => tag === b.token.tag);
const availableEth = new BigNumber(balance.tokens[0].value); const availableEth = new BigNumber(balance.tokens[0].value);
const available = new BigNumber(balance_.value);
const format = new BigNumber(balance_.token.format || 1); const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag);
const available = new BigNumber(senderBalance.value);
const format = new BigNumber(senderBalance.token.format || 1);
let { value, valueError } = this; let { value, valueError } = this;
let totalEth = gasTotal; let totalEth = gasTotal;
@ -409,26 +462,52 @@ export default class TransferStore {
return this._getTransferMethod().postTransaction(options, values); return this._getTransferMethod().postTransaction(options, values);
} }
estimateGas () { _estimateGas (forceToken = false) {
const { options, values } = this._getTransferParams(true); const { options, values } = this._getTransferParams(true, forceToken);
return this._getTransferMethod(true).estimateGas(options, values); return this._getTransferMethod(true, forceToken).estimateGas(options, values);
} }
_getTransferMethod (gas = false) { 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; const { isEth, isWallet } = this;
if (isEth && !isWallet) { if (isEth && !isWallet && !forceToken) {
return gas ? this.api.eth : this.api.parity; return gas ? this.api.eth : this.api.parity;
} }
if (isWallet) { if (isWallet && !forceToken) {
return this.wallet.instance.execute; return this.wallet.instance.execute;
} }
return this.token.contract.instance.transfer; return this.token.contract.instance.transfer;
} }
_getTransferParams (gas = false) { _getData (gas = false) {
const { isEth, isWallet } = this;
if (!isWallet || isEth) {
return this.data && this.data.length ? this.data : '';
}
const func = this._getTransferMethod(gas, true);
const { options, values } = this._getTransferParams(gas, true);
return this.token.contract.getCallData(func, options, values);
}
_getTransferParams (gas = false, forceToken = false) {
const { isEth, isWallet } = this; const { isEth, isWallet } = this;
const to = (isEth && !isWallet) ? this.recipient const to = (isEth && !isWallet) ? this.recipient
@ -446,27 +525,30 @@ export default class TransferStore {
options.gas = MAX_GAS_ESTIMATION; options.gas = MAX_GAS_ESTIMATION;
} }
if (isEth && !isWallet) { if (isEth && !isWallet && !forceToken) {
options.value = this.api.util.toWei(this.value || 0); options.value = this.api.util.toWei(this.value || 0);
options.data = this._getData(gas);
if (this.data && this.data.length) {
options.data = this.data;
}
return { options, values: [] }; return { options, values: [] };
} }
const values = isWallet if (isWallet && !forceToken) {
? [ const to = isEth ? this.recipient : this.token.contract.address;
this.recipient, const value = isEth ? this.api.util.toWei(this.value || 0) : new BigNumber(0);
this.api.util.toWei(this.value || 0),
this.data || '' const values = [
] to, value,
: [ this._getData(gas)
this.recipient,
new BigNumber(this.value || 0).mul(this.token.format).toFixed(0)
]; ];
return { options, values };
}
const values = [
this.recipient,
new BigNumber(this.value || 0).mul(this.token.format).toFixed(0)
];
return { options, values }; return { options, values };
} }

View File

@ -18,6 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { pick } from 'lodash';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
@ -25,7 +26,7 @@ import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { newError } from '~/ui/Errors/actions'; import { newError } from '~/ui/Errors/actions';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash, Input } from '~/ui';
import { nullableProptype } from '~/util/proptypes'; import { nullableProptype } from '~/util/proptypes';
import Details from './Details'; import Details from './Details';
@ -45,10 +46,10 @@ class Transfer extends Component {
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
images: PropTypes.object.isRequired, images: PropTypes.object.isRequired,
account: PropTypes.object,
senders: nullableProptype(PropTypes.object), senders: nullableProptype(PropTypes.object),
sendersBalances: nullableProptype(PropTypes.object),
account: PropTypes.object,
balance: PropTypes.object, balance: PropTypes.object,
balances: PropTypes.object,
wallet: PropTypes.object, wallet: PropTypes.object,
onClose: PropTypes.func onClose: PropTypes.func
} }
@ -133,6 +134,25 @@ class Transfer extends Component {
return ( return (
<CompletedStep> <CompletedStep>
<TxHash hash={ txhash } /> <TxHash hash={ txhash } />
{
this.store.operation
? (
<div>
<br />
<p>
This transaction needs confirmation from other owners.
<Input
style={ { width: '50%', margin: '0 auto' } }
value={ this.store.operation }
label='operation hash'
readOnly
allowCopy
/>
</p>
</div>
)
: null
}
</CompletedStep> </CompletedStep>
); );
} }
@ -277,7 +297,9 @@ function mapStateToProps (initState, initProps) {
return (state) => { return (state) => {
const { gasLimit } = state.nodeStatus; const { gasLimit } = state.nodeStatus;
return { gasLimit, wallet, senders }; const sendersBalances = senders ? pick(state.balances.balances, Object.keys(senders)) : null;
return { gasLimit, wallet, senders, sendersBalances };
}; };
} }

View File

@ -113,7 +113,7 @@ export function fetchTokens (_tokenIds) {
export function fetchBalances (_addresses) { export function fetchBalances (_addresses) {
return (dispatch, getState) => { return (dispatch, getState) => {
const { api, personal } = getState(); const { api, personal } = getState();
const { visibleAccounts } = personal; const { visibleAccounts, accounts } = personal;
const addresses = uniq(_addresses || visibleAccounts || []); const addresses = uniq(_addresses || visibleAccounts || []);
@ -123,12 +123,14 @@ export function fetchBalances (_addresses) {
const fullFetch = addresses.length === 1; const fullFetch = addresses.length === 1;
const fetchedAddresses = uniq(addresses.concat(Object.keys(accounts)));
return Promise return Promise
.all(addresses.map((addr) => fetchAccount(addr, api, fullFetch))) .all(fetchedAddresses.map((addr) => fetchAccount(addr, api, fullFetch)))
.then((accountsBalances) => { .then((accountsBalances) => {
const balances = {}; const balances = {};
addresses.forEach((addr, idx) => { fetchedAddresses.forEach((addr, idx) => {
balances[addr] = accountsBalances[idx]; balances[addr] = accountsBalances[idx];
}); });

View File

@ -14,16 +14,18 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isEqual, uniq, range } from 'lodash'; import { isEqual, uniq } from 'lodash';
import Contract from '../../api/contract'; import Contract from '~/api/contract';
import { wallet as WALLET_ABI } from '../../contracts/abi'; import { wallet as WALLET_ABI } from '~/contracts/abi';
import { bytesToHex, toHex } from '../../api/util/format'; import { bytesToHex, toHex } from '~/api/util/format';
import { ERROR_CODES } from '../../api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import { MAX_GAS_ESTIMATION } from '../../util/constants'; import { MAX_GAS_ESTIMATION } from '../../util/constants';
import { newError } from '../../ui/Errors/actions'; import WalletsUtils from '~/util/wallets';
import { newError } from '~/ui/Errors/actions';
const UPDATE_OWNERS = 'owners'; const UPDATE_OWNERS = 'owners';
const UPDATE_REQUIRE = 'require'; const UPDATE_REQUIRE = 'require';
@ -247,58 +249,9 @@ function fetchWalletInfo (contract, update, getState) {
} }
function fetchWalletTransactions (contract) { function fetchWalletTransactions (contract) {
const walletInstance = contract.instance; return WalletsUtils
const signatures = { .fetchTransactions(contract)
single: toHex(walletInstance.SingleTransact.signature), .then((transactions) => {
multi: toHex(walletInstance.MultiTransact.signature),
deposit: toHex(walletInstance.Deposit.signature)
};
return contract
.getAllLogs({
topics: [ [ signatures.single, signatures.multi, signatures.deposit ] ]
})
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logB.blockNumber.comparedTo(logA.blockNumber);
if (comp !== 0) {
return comp;
}
return logB.transactionIndex.comparedTo(logA.transactionIndex);
});
})
.then((logs) => {
const transactions = logs.map((log) => {
const signature = toHex(log.topics[0]);
const value = log.params.value.value;
const from = signature === signatures.deposit
? log.params['_from'].value
: contract.address;
const to = signature === signatures.deposit
? contract.address
: log.params.to.value;
const transaction = {
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
from, to, value
};
if (log.params.operation) {
transaction.operation = bytesToHex(log.params.operation.value);
}
if (log.params.data) {
transaction.data = log.params.data.value;
}
return transaction;
});
return { return {
key: UPDATE_TRANSACTIONS, key: UPDATE_TRANSACTIONS,
value: transactions value: transactions
@ -307,13 +260,8 @@ function fetchWalletTransactions (contract) {
} }
function fetchWalletOwners (contract) { function fetchWalletOwners (contract) {
const walletInstance = contract.instance; return WalletsUtils
.fetchOwners(contract)
return walletInstance
.m_numOwners.call()
.then((mNumOwners) => {
return Promise.all(range(mNumOwners.toNumber()).map((idx) => walletInstance.getOwner.call({}, [ idx ])));
})
.then((value) => { .then((value) => {
return { return {
key: UPDATE_OWNERS, key: UPDATE_OWNERS,
@ -323,10 +271,8 @@ function fetchWalletOwners (contract) {
} }
function fetchWalletRequire (contract) { function fetchWalletRequire (contract) {
const walletInstance = contract.instance; return WalletsUtils
.fetchRequire(contract)
return walletInstance
.m_required.call()
.then((value) => { .then((value) => {
return { return {
key: UPDATE_REQUIRE, key: UPDATE_REQUIRE,
@ -336,22 +282,12 @@ function fetchWalletRequire (contract) {
} }
function fetchWalletDailylimit (contract) { function fetchWalletDailylimit (contract) {
const walletInstance = contract.instance; return WalletsUtils
.fetchDailylimit(contract)
return Promise .then((value) => {
.all([
walletInstance.m_dailyLimit.call(),
walletInstance.m_spentToday.call(),
walletInstance.m_lastDay.call()
])
.then((values) => {
return { return {
key: UPDATE_DAILYLIMIT, key: UPDATE_DAILYLIMIT,
value: { value
limit: values[0],
spent: values[1],
last: values[2]
}
}; };
}); });
} }

View File

@ -33,7 +33,7 @@ export default class ConfirmDialog extends Component {
iconDeny: PropTypes.node, iconDeny: PropTypes.node,
labelConfirm: PropTypes.string, labelConfirm: PropTypes.string,
labelDeny: PropTypes.string, labelDeny: PropTypes.string,
title: nodeOrStringProptype.isRequired, title: nodeOrStringProptype().isRequired,
visible: PropTypes.bool.isRequired, visible: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
onDeny: PropTypes.func.isRequired onDeny: PropTypes.func.isRequired

View File

@ -23,8 +23,8 @@ import styles from './title.css';
export default class Title extends Component { export default class Title extends Component {
static propTypes = { static propTypes = {
className: PropTypes.string, className: PropTypes.string,
title: nodeOrStringProptype, title: nodeOrStringProptype(),
byline: nodeOrStringProptype byline: nodeOrStringProptype()
} }
state = { state = {

View File

@ -30,7 +30,7 @@ export default class Container extends Component {
compact: PropTypes.bool, compact: PropTypes.bool,
light: PropTypes.bool, light: PropTypes.bool,
style: PropTypes.object, style: PropTypes.object,
title: nodeOrStringProptype title: nodeOrStringProptype()
} }
render () { render () {

View File

@ -120,7 +120,7 @@ export default class AutoComplete extends Component {
switch (keycode(event)) { switch (keycode(event)) {
case 'down': case 'down':
const { menu } = muiAutocomplete.refs; const { menu } = muiAutocomplete.refs;
menu.handleKeyDown(event); menu && menu.handleKeyDown(event);
this.setState({ fakeBlur: true }); this.setState({ fakeBlur: true });
break; break;
@ -133,7 +133,7 @@ export default class AutoComplete extends Component {
const e = new CustomEvent('down'); const e = new CustomEvent('down');
e.which = 40; e.which = 40;
muiAutocomplete.handleKeyDown(e); muiAutocomplete && muiAutocomplete.handleKeyDown(e);
break; break;
} }
} }

View File

@ -66,7 +66,8 @@ export default class Input extends Component {
PropTypes.number, PropTypes.string PropTypes.number, PropTypes.string
]), ]),
min: PropTypes.any, min: PropTypes.any,
max: PropTypes.any max: PropTypes.any,
style: PropTypes.object
}; };
static defaultProps = { static defaultProps = {
@ -74,7 +75,8 @@ export default class Input extends Component {
readOnly: false, readOnly: false,
allowCopy: false, allowCopy: false,
hideUnderline: false, hideUnderline: false,
floatCopy: false floatCopy: false,
style: {}
} }
state = { state = {
@ -89,7 +91,8 @@ export default class Input extends Component {
render () { render () {
const { value } = this.state; const { value } = this.state;
const { children, className, hideUnderline, disabled, error, label, hint, multiLine, rows, type, min, max } = this.props; const { children, className, hideUnderline, disabled, error, label } = this.props;
const { hint, multiLine, rows, type, min, max, style } = this.props;
const readOnly = this.props.readOnly || disabled; const readOnly = this.props.readOnly || disabled;
@ -105,7 +108,7 @@ export default class Input extends Component {
} }
return ( return (
<div className={ styles.container }> <div className={ styles.container } style={ style }>
{ this.renderCopyButton() } { this.renderCopyButton() }
<TextField <TextField
autoComplete='off' autoComplete='off'

View File

@ -35,7 +35,7 @@ export default class InputInline extends Component {
value: PropTypes.oneOfType([ value: PropTypes.oneOfType([
PropTypes.number, PropTypes.string PropTypes.number, PropTypes.string
]), ]),
static: nodeOrStringProptype static: nodeOrStringProptype()
} }
state = { state = {

View File

@ -37,7 +37,10 @@ export default class RadioButtons extends Component {
render () { render () {
const { value, values } = this.props; const { value, values } = this.props;
const index = parseInt(value); 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); const key = this.getKey(selectedValue, index);

View File

@ -40,7 +40,14 @@ export default class TypedInput extends Component {
error: PropTypes.any, error: PropTypes.any,
value: PropTypes.any, value: PropTypes.any,
label: PropTypes.string, label: PropTypes.string,
hint: PropTypes.string hint: PropTypes.string,
min: PropTypes.number,
max: PropTypes.number
};
static defaultProps = {
min: null,
max: null
}; };
render () { render () {
@ -90,16 +97,22 @@ export default class TypedInput extends Component {
}; };
const style = { const style = {
width: 32, width: 24,
height: 32, height: 24,
padding: 0 padding: 0
}; };
const plusStyle = {
...style,
backgroundColor: 'rgba(255, 255, 255, 0.25)',
borderRadius: '50%'
};
return ( return (
<div> <div style={ { marginTop: '0.75em' } }>
<IconButton <IconButton
iconStyle={ iconStyle } iconStyle={ iconStyle }
style={ style } style={ plusStyle }
onTouchTap={ this.onAddField } onTouchTap={ this.onAddField }
> >
<AddIcon /> <AddIcon />
@ -145,7 +158,7 @@ export default class TypedInput extends Component {
} }
renderNumber () { renderNumber () {
const { label, value, error, param, hint } = this.props; const { label, value, error, param, hint, min, max } = this.props;
return ( return (
<Input <Input
@ -153,9 +166,10 @@ export default class TypedInput extends Component {
hint={ hint } hint={ hint }
value={ value } value={ value }
error={ error } error={ error }
onSubmit={ this.onSubmit } onChange={ this.onChange }
type='number' type='number'
min={ param.signed ? null : 0 } min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null }
/> />
); );
} }

View File

@ -28,7 +28,7 @@ export default class Title extends Component {
current: PropTypes.number, current: PropTypes.number,
steps: PropTypes.array, steps: PropTypes.array,
waiting: PropTypes.array, waiting: PropTypes.array,
title: nodeOrStringProptype title: nodeOrStringProptype()
} }
render () { render () {

View File

@ -44,7 +44,7 @@ class Modal extends Component {
current: PropTypes.number, current: PropTypes.number,
waiting: PropTypes.array, waiting: PropTypes.array,
steps: PropTypes.array, steps: PropTypes.array,
title: nodeOrStringProptype, title: nodeOrStringProptype(),
visible: PropTypes.bool.isRequired, visible: PropTypes.bool.isRequired,
settings: PropTypes.object.isRequired settings: PropTypes.object.isRequired
} }

107
js/src/util/wallets.js Normal file
View File

@ -0,0 +1,107 @@
// Copyright 2015, 2016 Ethcore (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 { range } from 'lodash';
import { bytesToHex, toHex } from '~/api/util/format';
export default class WalletsUtils {
static fetchRequire (walletContract) {
return walletContract.instance.m_required.call();
}
static fetchOwners (walletContract) {
const walletInstance = walletContract.instance;
return walletInstance
.m_numOwners.call()
.then((mNumOwners) => {
return Promise.all(range(mNumOwners.toNumber()).map((idx) => walletInstance.getOwner.call({}, [ idx ])));
});
}
static fetchDailylimit (walletContract) {
const walletInstance = walletContract.instance;
return Promise
.all([
walletInstance.m_dailyLimit.call(),
walletInstance.m_spentToday.call(),
walletInstance.m_lastDay.call()
])
.then(([ limit, spent, last ]) => ({
limit, spent, last
}));
}
static fetchTransactions (walletContract) {
const walletInstance = walletContract.instance;
const signatures = {
single: toHex(walletInstance.SingleTransact.signature),
multi: toHex(walletInstance.MultiTransact.signature),
deposit: toHex(walletInstance.Deposit.signature)
};
return walletContract
.getAllLogs({
topics: [ [ signatures.single, signatures.multi, signatures.deposit ] ]
})
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logB.blockNumber.comparedTo(logA.blockNumber);
if (comp !== 0) {
return comp;
}
return logB.transactionIndex.comparedTo(logA.transactionIndex);
});
})
.then((logs) => {
const transactions = logs.map((log) => {
const signature = toHex(log.topics[0]);
const value = log.params.value.value;
const from = signature === signatures.deposit
? log.params['_from'].value
: walletContract.address;
const to = signature === signatures.deposit
? walletContract.address
: log.params.to.value;
const transaction = {
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
from, to, value
};
if (log.params.operation) {
transaction.operation = bytesToHex(log.params.operation.value);
}
if (log.params.data) {
transaction.data = log.params.data.value;
}
return transaction;
});
return transactions;
});
}
}

View File

@ -25,16 +25,23 @@ import styles from './header.css';
export default class Header extends Component { export default class Header extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object api: PropTypes.object
} };
static propTypes = { static propTypes = {
account: PropTypes.object, account: PropTypes.object,
balance: PropTypes.object balance: PropTypes.object,
} className: PropTypes.string,
children: PropTypes.node
};
static defaultProps = {
className: '',
children: null
};
render () { render () {
const { api } = this.context; const { api } = this.context;
const { account, balance } = this.props; const { account, balance, className, children } = this.props;
const { address, meta, uuid } = account; const { address, meta, uuid } = account;
if (!account) { if (!account) {
@ -46,7 +53,7 @@ export default class Header extends Component {
: <div className={ styles.uuidline }>uuid: { uuid }</div>; : <div className={ styles.uuidline }>uuid: { uuid }</div>;
return ( return (
<div> <div className={ className }>
<Container> <Container>
<IdentityIcon <IdentityIcon
address={ address } /> address={ address } />
@ -74,6 +81,7 @@ export default class Header extends Component {
dappsUrl={ api.dappsUrl } dappsUrl={ api.dappsUrl }
/> />
</div> </div>
{ children }
</Container> </Container>
</div> </div>
); );

View File

@ -86,8 +86,15 @@ class Accounts extends Component {
{ this.renderNewWalletDialog() } { this.renderNewWalletDialog() }
{ this.renderActionbar() } { this.renderActionbar() }
{ this.renderAccounts() } <Page>
{ this.renderWallets() } <Tooltip
className={ styles.accountTooltip }
text='your accounts are visible for easy access, allowing you to edit the meta information, make transfers, view transactions and fund the account'
/>
{ this.renderWallets() }
{ this.renderAccounts() }
</Page>
</div> </div>
); );
} }
@ -115,18 +122,13 @@ class Accounts extends Component {
const { searchValues, sortOrder } = this.state; const { searchValues, sortOrder } = this.state;
return ( return (
<Page> <List
<List search={ searchValues }
search={ searchValues } accounts={ accounts }
accounts={ accounts } balances={ balances }
balances={ balances } empty={ !hasAccounts }
empty={ !hasAccounts } order={ sortOrder }
order={ sortOrder } handleAddSearchToken={ this.onAddSearchToken } />
handleAddSearchToken={ this.onAddSearchToken } />
<Tooltip
className={ styles.accountTooltip }
text='your accounts are visible for easy access, allowing you to edit the meta information, make transfers, view transactions and fund the account' />
</Page>
); );
} }
@ -139,17 +141,15 @@ class Accounts extends Component {
const { searchValues, sortOrder } = this.state; const { searchValues, sortOrder } = this.state;
return ( return (
<Page> <List
<List link='wallet'
link='wallet' search={ searchValues }
search={ searchValues } accounts={ wallets }
accounts={ wallets } balances={ balances }
balances={ balances } empty={ !hasWallets }
empty={ !hasWallets } order={ sortOrder }
order={ sortOrder } handleAddSearchToken={ this.onAddSearchToken }
handleAddSearchToken={ this.onAddSearchToken } />
/>
</Page>
); );
} }

View File

@ -19,7 +19,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { ConfirmDialog, IdentityIcon, IdentityName } from '~/ui'; import { ConfirmDialog, IdentityIcon, IdentityName } from '~/ui';
import { newError } from '../../../redux/actions'; import { newError } from '~/redux/actions';
import styles from '../address.css'; import styles from '../address.css';
@ -27,16 +27,17 @@ class Delete extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
router: PropTypes.object router: PropTypes.object
} };
static propTypes = { static propTypes = {
route: PropTypes.string.isRequired,
address: PropTypes.string, address: PropTypes.string,
account: PropTypes.object, account: PropTypes.object,
route: PropTypes.string.isRequired,
visible: PropTypes.bool, visible: PropTypes.bool,
onClose: PropTypes.func, onClose: PropTypes.func,
newError: PropTypes.func newError: PropTypes.func
} };
render () { render () {
const { account, visible } = this.props; const { account, visible } = this.props;

View File

@ -23,7 +23,7 @@ import ContentCreate from 'material-ui/svg-icons/content/create';
import EyeIcon from 'material-ui/svg-icons/image/remove-red-eye'; import EyeIcon from 'material-ui/svg-icons/image/remove-red-eye';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { newError } from '../../redux/actions'; import { newError } from '~/redux/actions';
import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { EditMeta, ExecuteContract } from '~/modals'; import { EditMeta, ExecuteContract } from '~/modals';

View File

@ -19,7 +19,7 @@ import { action, computed, observable, transaction } from 'mobx';
import store from 'store'; import store from 'store';
import Contracts from '~/contracts'; import Contracts from '~/contracts';
import { hashToImageUrl } from '../../redux/util'; import { hashToImageUrl } from '~/redux/util';
import builtinApps from './builtin.json'; import builtinApps from './builtin.json';

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import ReactTooltip from 'react-tooltip'; import ReactTooltip from 'react-tooltip';
import { MethodDecoding } from '../../../../ui'; import { MethodDecoding } from '~/ui';
import * as tUtil from '../util/transaction'; import * as tUtil from '../util/transaction';
import Account from '../Account'; import Account from '../Account';

View File

@ -64,7 +64,7 @@ export default class TransactionPending extends Component {
} }
render () { render () {
const { className, id, transaction, store } = this.props; const { className, id, transaction, store, isTest } = this.props;
const { from, value } = transaction; const { from, value } = transaction;
const { totalValue } = this.state; const { totalValue } = this.state;
@ -76,6 +76,7 @@ export default class TransactionPending extends Component {
id={ id } id={ id }
value={ value } value={ value }
from={ from } from={ from }
isTest={ isTest }
fromBalance={ fromBalance } fromBalance={ fromBalance }
className={ styles.transactionDetails } className={ styles.transactionDetails }
transaction={ transaction } transaction={ transaction }

View File

@ -17,6 +17,6 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { identity } from '../util'; import { identity } from '../util';
import { withError } from '../../../redux/util'; import { withError } from '~/redux/util';
export const copyToClipboard = createAction('copy toClipboard', identity, withError(identity)); export const copyToClipboard = createAction('copy toClipboard', identity, withError(identity));

View File

@ -17,7 +17,7 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { identity } from '../util'; import { identity } from '../util';
import { withError } from '../../../redux/util'; import { withError } from '~/redux/util';
export const updateLogging = createAction( export const updateLogging = createAction(
'update logging', identity, withError(flag => `logging updated to ${flag}`) 'update logging', identity, withError(flag => `logging updated to ${flag}`)

View File

@ -17,7 +17,7 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { identity } from '../util'; import { identity } from '../util';
import { withError } from '../../../redux/util'; import { withError } from '~/redux/util';
export const error = createAction('error rpc', identity, export const error = createAction('error rpc', identity,
withError(() => 'error processing rpc call. check console for details', 'error') withError(() => 'error processing rpc call. check console for details', 'error')

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from '../../../../redux/actions'; import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from '~/redux/actions';
import Debug from '../../components/Debug'; import Debug from '../../components/Debug';
import Status from '../../components/Status'; import Status from '../../components/Status';

View File

@ -20,13 +20,13 @@ import ReactTooltip from 'react-tooltip';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { confirmOperation, revokeOperation } from '../../../redux/providers/walletActions'; import { confirmOperation, revokeOperation } from '~/redux/providers/walletActions';
import { bytesToHex } from '../../../api/util/format'; import { bytesToHex } from '~/api/util/format';
import { Container, InputAddress, Button, IdentityIcon } from '../../../ui'; import { Container, InputAddress, Button, IdentityIcon } from '~/ui';
import { TxRow } from '../../../ui/TxList/txList'; import { TxRow } from '~/ui/TxList/txList';
import styles from '../wallet.css'; import styles from '../wallet.css';
import txListStyles from '../../../ui/TxList/txList.css'; import txListStyles from '~/ui/TxList/txList.css';
class WalletConfirmations extends Component { class WalletConfirmations extends Component {
static contextTypes = { static contextTypes = {

View File

@ -15,9 +15,8 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import moment from 'moment';
import { Container, InputAddress } from '../../../ui'; import { Container, InputAddress } from '~/ui';
import styles from '../wallet.css'; import styles from '../wallet.css';
@ -29,18 +28,21 @@ export default class WalletDetails extends Component {
static propTypes = { static propTypes = {
owners: PropTypes.array, owners: PropTypes.array,
require: PropTypes.object, require: PropTypes.object,
dailylimit: PropTypes.object className: PropTypes.string
};
static defaultProps = {
className: ''
}; };
render () { render () {
return ( const { className } = this.props;
<div className={ styles.details }>
<Container title='Owners'>
{ this.renderOwners() }
</Container>
return (
<div className={ [ styles.details, className ].join(' ') }>
<Container title='Details'> <Container title='Details'>
{ this.renderDetails() } { this.renderDetails() }
{ this.renderOwners() }
</Container> </Container>
</div> </div>
); );
@ -70,17 +72,12 @@ export default class WalletDetails extends Component {
} }
renderDetails () { renderDetails () {
const { require, dailylimit } = this.props; const { require } = this.props;
const { api } = this.context;
if (!dailylimit || !dailylimit.limit) { if (!require) {
return null; return null;
} }
const limit = api.util.fromWei(dailylimit.limit).toFormat(3);
const spent = api.util.fromWei(dailylimit.spent).toFormat(3);
const date = moment(dailylimit.last.toNumber() * 24 * 3600 * 1000);
return ( return (
<div> <div>
<p> <p>
@ -88,14 +85,6 @@ export default class WalletDetails extends Component {
<span className={ styles.detail }>{ require.toFormat() } owners</span> <span className={ styles.detail }>{ require.toFormat() } owners</span>
<span>to validate any action (transactions, modifications).</span> <span>to validate any action (transactions, modifications).</span>
</p> </p>
<p>
<span className={ styles.detail }>{ spent }<span className={ styles.eth } /></span>
<span>has been spent today, out of</span>
<span className={ styles.detail }>{ limit }<span className={ styles.eth } /></span>
<span>set as the daily limit, which has been reset on</span>
<span className={ styles.detail }>{ date.format('LL') }</span>
</p>
</div> </div>
); );
} }

View File

@ -16,11 +16,11 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { bytesToHex } from '../../../api/util/format'; import { bytesToHex } from '~/api/util/format';
import { Container } from '../../../ui'; import { Container } from '~/ui';
import { TxRow } from '../../../ui/TxList/txList'; import { TxRow } from '~/ui/TxList/txList';
import txListStyles from '../../../ui/TxList/txList.css'; import txListStyles from '~/ui/TxList/txList.css';
export default class WalletTransactions extends Component { export default class WalletTransactions extends Component {
static propTypes = { static propTypes = {

View File

@ -23,7 +23,6 @@
> * { > * {
flex: 1; flex: 1;
margin: 0.125em;
height: auto; height: auto;
&:first-child { &:first-child {
@ -36,6 +35,38 @@
} }
} }
.owners {
margin-top: 0.75em;
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 1em 0.5em 0.5em;
> * {
margin-bottom: 0.5em;
}
}
.info {
display: flex;
flex-direction: row;
.header {
flex: 1;
margin-right: 0.25em;
}
.details {
flex: 1;
margin-left: 0.25em;
}
> * {
height: auto;
}
}
.detail { .detail {
font-size: 1.125em; font-size: 1.125em;
color: white; color: white;

View File

@ -17,18 +17,23 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import moment from 'moment';
import ContentCreate from 'material-ui/svg-icons/content/create'; import ContentCreate from 'material-ui/svg-icons/content/create';
import ActionDelete from 'material-ui/svg-icons/action/delete';
import ContentSend from 'material-ui/svg-icons/content/send'; import ContentSend from 'material-ui/svg-icons/content/send';
import { EditMeta, Transfer } from '../../modals'; import { nullableProptype } from '~/util/proptypes';
import { Actionbar, Button, Page, Loading } from '../../ui'; import { EditMeta, Transfer } from '~/modals';
import { Actionbar, Button, Page, Loading } from '~/ui';
import Delete from '../Address/Delete';
import Header from '../Account/Header'; import Header from '../Account/Header';
import WalletDetails from './Details'; import WalletDetails from './Details';
import WalletConfirmations from './Confirmations'; import WalletConfirmations from './Confirmations';
import WalletTransactions from './Transactions'; import WalletTransactions from './Transactions';
import { setVisibleAccounts } from '../../redux/providers/personalActions'; import { setVisibleAccounts } from '~/redux/providers/personalActions';
import styles from './wallet.css'; import styles from './wallet.css';
@ -59,17 +64,18 @@ class Wallet extends Component {
static propTypes = { static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
balance: nullableProptype(PropTypes.object.isRequired),
images: PropTypes.object.isRequired, images: PropTypes.object.isRequired,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
wallets: PropTypes.object.isRequired, wallets: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired, wallet: PropTypes.object.isRequired,
balances: PropTypes.object.isRequired,
isTest: PropTypes.bool.isRequired isTest: PropTypes.bool.isRequired
}; };
state = { state = {
showEditDialog: false, showEditDialog: false,
showTransferDialog: false showTransferDialog: false,
showDeleteDialog: false
}; };
componentDidMount () { componentDidMount () {
@ -96,34 +102,74 @@ class Wallet extends Component {
} }
render () { render () {
const { wallets, balances, address } = this.props; const { wallets, balance, address } = this.props;
const wallet = (wallets || {})[address]; const wallet = (wallets || {})[address];
const balance = (balances || {})[address];
if (!wallet) { if (!wallet) {
return null; return null;
} }
const { owners, require, dailylimit } = this.props.wallet;
return ( return (
<div className={ styles.wallet }> <div className={ styles.wallet }>
{ this.renderEditDialog(wallet) } { this.renderEditDialog(wallet) }
{ this.renderTransferDialog() } { this.renderTransferDialog() }
{ this.renderDeleteDialog(wallet) }
{ this.renderActionbar() } { this.renderActionbar() }
<Page> <Page>
<Header <div className={ styles.info }>
account={ wallet } <Header
balance={ balance } className={ styles.header }
/> account={ wallet }
balance={ balance }
>
{ this.renderInfos() }
</Header>
<WalletDetails
className={ styles.details }
owners={ owners }
require={ require }
dailylimit={ dailylimit }
/>
</div>
{ this.renderDetails() } { this.renderDetails() }
</Page> </Page>
</div> </div>
); );
} }
renderInfos () {
const { dailylimit } = this.props.wallet;
const { api } = this.context;
if (!dailylimit || !dailylimit.limit) {
return null;
}
const limit = api.util.fromWei(dailylimit.limit).toFormat(3);
const spent = api.util.fromWei(dailylimit.spent).toFormat(3);
const date = moment(dailylimit.last.toNumber() * 24 * 3600 * 1000);
return (
<div>
<br />
<p>
<span className={ styles.detail }>{ spent }<span className={ styles.eth } /></span>
<span>has been spent today, out of</span>
<span className={ styles.detail }>{ limit }<span className={ styles.eth } /></span>
<span>set as the daily limit, which has been reset on</span>
<span className={ styles.detail }>{ date.format('LL') }</span>
</p>
</div>
);
}
renderDetails () { renderDetails () {
const { address, isTest, wallet } = this.props; const { address, isTest, wallet } = this.props;
const { owners, require, dailylimit, confirmations, transactions } = wallet; const { owners, require, confirmations, transactions } = wallet;
if (!isTest || !owners || !require) { if (!isTest || !owners || !require) {
return ( return (
@ -134,13 +180,6 @@ class Wallet extends Component {
} }
return [ return [
<WalletDetails
key='details'
owners={ owners }
require={ require }
dailylimit={ dailylimit }
/>,
<WalletConfirmations <WalletConfirmations
key='confirmations' key='confirmations'
owners={ owners } owners={ owners }
@ -160,9 +199,7 @@ class Wallet extends Component {
} }
renderActionbar () { renderActionbar () {
const { address, balances } = this.props; const { balance } = this.props;
const balance = balances[address];
const showTransferButton = !!(balance && balance.tokens); const showTransferButton = !!(balance && balance.tokens);
const buttons = [ const buttons = [
@ -172,6 +209,11 @@ class Wallet extends Component {
label='transfer' label='transfer'
disabled={ !showTransferButton } disabled={ !showTransferButton }
onClick={ this.onTransferClick } />, onClick={ this.onTransferClick } />,
<Button
key='delete'
icon={ <ActionDelete /> }
label='delete wallet'
onClick={ this.showDeleteDialog } />,
<Button <Button
key='editmeta' key='editmeta'
icon={ <ContentCreate /> } icon={ <ContentCreate /> }
@ -186,6 +228,18 @@ class Wallet extends Component {
); );
} }
renderDeleteDialog (account) {
const { showDeleteDialog } = this.state;
return (
<Delete
account={ account }
visible={ showDeleteDialog }
route='/accounts'
onClose={ this.closeDeleteDialog } />
);
}
renderEditDialog (wallet) { renderEditDialog (wallet) {
const { showEditDialog } = this.state; const { showEditDialog } = this.state;
@ -208,15 +262,13 @@ class Wallet extends Component {
return null; return null;
} }
const { wallets, balances, images, address } = this.props; const { wallets, balance, images, address } = this.props;
const wallet = wallets[address]; const wallet = wallets[address];
const balance = balances[address];
return ( return (
<Transfer <Transfer
account={ wallet } account={ wallet }
balance={ balance } balance={ balance }
balances={ balances }
images={ images } images={ images }
onClose={ this.onTransferClose } onClose={ this.onTransferClose }
/> />
@ -238,6 +290,14 @@ class Wallet extends Component {
onTransferClose = () => { onTransferClose = () => {
this.onTransferClick(); this.onTransferClick();
} }
closeDeleteDialog = () => {
this.setState({ showDeleteDialog: false });
}
showDeleteDialog = () => {
this.setState({ showDeleteDialog: true });
}
} }
function mapStateToProps (_, initProps) { function mapStateToProps (_, initProps) {
@ -248,12 +308,14 @@ function mapStateToProps (_, initProps) {
const { wallets } = state.personal; const { wallets } = state.personal;
const { balances } = state.balances; const { balances } = state.balances;
const { images } = state; const { images } = state;
const wallet = state.wallet.wallets[address] || {}; const wallet = state.wallet.wallets[address] || {};
const balance = balances[address] || null;
return { return {
isTest, isTest,
wallets, wallets,
balances, balance,
images, images,
address, address,
wallet wallet

View File

@ -28,6 +28,7 @@ use ethcore::service::ClientService;
use ethcore::client::{Mode, DatabaseCompactionProfile, VMType, BlockImportError, BlockChainClient, BlockID}; use ethcore::client::{Mode, DatabaseCompactionProfile, VMType, BlockImportError, BlockChainClient, BlockID};
use ethcore::error::ImportError; use ethcore::error::ImportError;
use ethcore::miner::Miner; use ethcore::miner::Miner;
use ethcore::verification::queue::VerifierSettings;
use cache::CacheConfig; use cache::CacheConfig;
use informant::{Informant, MillisecondDuration}; use informant::{Informant, MillisecondDuration};
use params::{SpecType, Pruning, Switch, tracing_switch_to_bool, fatdb_switch_to_bool}; use params::{SpecType, Pruning, Switch, tracing_switch_to_bool, fatdb_switch_to_bool};
@ -84,6 +85,7 @@ pub struct ImportBlockchain {
pub vm_type: VMType, pub vm_type: VMType,
pub check_seal: bool, pub check_seal: bool,
pub with_color: bool, pub with_color: bool,
pub verifier_settings: VerifierSettings,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -175,7 +177,21 @@ fn execute_import(cmd: ImportBlockchain) -> Result<String, String> {
try!(execute_upgrades(&db_dirs, algorithm, cmd.compaction.compaction_profile(db_dirs.fork_path().as_path()))); try!(execute_upgrades(&db_dirs, algorithm, cmd.compaction.compaction_profile(db_dirs.fork_path().as_path())));
// prepare client config // prepare client config
let client_config = to_client_config(&cmd.cache_config, Mode::Active, tracing, fat_db, cmd.compaction, cmd.wal, cmd.vm_type, "".into(), algorithm, cmd.pruning_history, cmd.check_seal); let mut client_config = to_client_config(
&cmd.cache_config,
Mode::Active,
tracing,
fat_db,
cmd.compaction,
cmd.wal,
cmd.vm_type,
"".into(),
algorithm,
cmd.pruning_history,
cmd.check_seal
);
client_config.queue.verifier_settings = cmd.verifier_settings;
// build client // build client
let service = try!(ClientService::start( let service = try!(ClientService::start(

View File

@ -95,6 +95,8 @@ cache_size = 128 # Overrides above caches with total size
fast_and_loose = false fast_and_loose = false
db_compaction = "ssd" db_compaction = "ssd"
fat_db = "auto" fat_db = "auto"
scale_verifiers = true
num_verifiers = 6
[snapshots] [snapshots]
disable_periodic = false disable_periodic = false

View File

@ -58,6 +58,7 @@ cache_size_queue = 100
cache_size_state = 25 cache_size_state = 25
db_compaction = "ssd" db_compaction = "ssd"
fat_db = "off" fat_db = "off"
scale_verifiers = false
[snapshots] [snapshots]
disable_periodic = true disable_periodic = true

View File

@ -244,6 +244,10 @@ usage! {
or |c: &Config| otry!(c.footprint).db_compaction.clone(), or |c: &Config| otry!(c.footprint).db_compaction.clone(),
flag_fat_db: String = "auto", flag_fat_db: String = "auto",
or |c: &Config| otry!(c.footprint).fat_db.clone(), or |c: &Config| otry!(c.footprint).fat_db.clone(),
flag_scale_verifiers: bool = false,
or |c: &Config| otry!(c.footprint).scale_verifiers.clone(),
flag_num_verifiers: Option<usize> = None,
or |c: &Config| otry!(c.footprint).num_verifiers.clone().map(Some),
// -- Import/Export Options // -- Import/Export Options
flag_from: String = "1", or |_| None, flag_from: String = "1", or |_| None,
@ -405,6 +409,8 @@ struct Footprint {
cache_size_state: Option<u32>, cache_size_state: Option<u32>,
db_compaction: Option<String>, db_compaction: Option<String>,
fat_db: Option<String>, fat_db: Option<String>,
scale_verifiers: Option<bool>,
num_verifiers: Option<usize>,
} }
#[derive(Default, Debug, PartialEq, RustcDecodable)] #[derive(Default, Debug, PartialEq, RustcDecodable)]
@ -606,6 +612,8 @@ mod tests {
flag_fast_and_loose: false, flag_fast_and_loose: false,
flag_db_compaction: "ssd".into(), flag_db_compaction: "ssd".into(),
flag_fat_db: "auto".into(), flag_fat_db: "auto".into(),
flag_scale_verifiers: true,
flag_num_verifiers: Some(6),
// -- Import/Export Options // -- Import/Export Options
flag_from: "1".into(), flag_from: "1".into(),
@ -776,6 +784,8 @@ mod tests {
cache_size_state: Some(25), cache_size_state: Some(25),
db_compaction: Some("ssd".into()), db_compaction: Some("ssd".into()),
fat_db: Some("off".into()), fat_db: Some("off".into()),
scale_verifiers: Some(false),
num_verifiers: None,
}), }),
snapshots: Some(Snapshots { snapshots: Some(Snapshots {
disable_periodic: Some(true), disable_periodic: Some(true),

View File

@ -254,7 +254,7 @@ Footprint Options:
the state cache (default: {flag_cache_size_state}). the state cache (default: {flag_cache_size_state}).
--cache-size MB Set total amount of discretionary memory to use for --cache-size MB Set total amount of discretionary memory to use for
the entire system, overrides other cache and queue the entire system, overrides other cache and queue
options.a (default: {flag_cache_size:?}) options. (default: {flag_cache_size:?})
--fast-and-loose Disables DB WAL, which gives a significant speed up --fast-and-loose Disables DB WAL, which gives a significant speed up
but means an unclean exit is unrecoverable. (default: {flag_fast_and_loose}) but means an unclean exit is unrecoverable. (default: {flag_fast_and_loose})
--db-compaction TYPE Database compaction type. TYPE may be one of: --db-compaction TYPE Database compaction type. TYPE may be one of:
@ -265,6 +265,11 @@ Footprint Options:
of all accounts and storage keys. Doubles the size of all accounts and storage keys. Doubles the size
of the state database. BOOL may be one of on, off of the state database. BOOL may be one of on, off
or auto. (default: {flag_fat_db}) or auto. (default: {flag_fat_db})
--scale-verifiers Automatically scale amount of verifier threads based on
workload. Not guaranteed to be faster.
(default: {flag_scale_verifiers})
--num-verifiers INT Amount of verifier threads to use or to begin with, if verifier
auto-scaling is enabled. (default: {flag_num_verifiers:?})
Import/Export Options: Import/Export Options:
--from BLOCK Export from block BLOCK, which may be an index or --from BLOCK Export from block BLOCK, which may be an index or

View File

@ -25,6 +25,7 @@ use util::log::Colour;
use ethsync::{NetworkConfiguration, is_valid_node_url, AllowIP}; use ethsync::{NetworkConfiguration, is_valid_node_url, AllowIP};
use ethcore::client::VMType; use ethcore::client::VMType;
use ethcore::miner::{MinerOptions, Banning}; use ethcore::miner::{MinerOptions, Banning};
use ethcore::verification::queue::VerifierSettings;
use rpc::{IpcConfiguration, HttpConfiguration}; use rpc::{IpcConfiguration, HttpConfiguration};
use ethcore_rpc::NetworkSettings; use ethcore_rpc::NetworkSettings;
@ -158,6 +159,7 @@ impl Configuration {
vm_type: vm_type, vm_type: vm_type,
check_seal: !self.args.flag_no_seal_check, check_seal: !self.args.flag_no_seal_check,
with_color: logger_config.color, with_color: logger_config.color,
verifier_settings: self.verifier_settings(),
}; };
Cmd::Blockchain(BlockchainCmd::Import(import_cmd)) Cmd::Blockchain(BlockchainCmd::Import(import_cmd))
} else if self.args.cmd_export { } else if self.args.cmd_export {
@ -241,6 +243,8 @@ impl Configuration {
None None
}; };
let verifier_settings = self.verifier_settings();
let run_cmd = RunCmd { let run_cmd = RunCmd {
cache_config: cache_config, cache_config: cache_config,
dirs: dirs, dirs: dirs,
@ -275,6 +279,7 @@ impl Configuration {
no_periodic_snapshot: self.args.flag_no_periodic_snapshot, no_periodic_snapshot: self.args.flag_no_periodic_snapshot,
check_seal: !self.args.flag_no_seal_check, check_seal: !self.args.flag_no_seal_check,
download_old_blocks: !self.args.flag_no_ancient_blocks, download_old_blocks: !self.args.flag_no_ancient_blocks,
verifier_settings: verifier_settings,
}; };
Cmd::Run(run_cmd) Cmd::Run(run_cmd)
}; };
@ -707,6 +712,16 @@ impl Configuration {
!ui_disabled !ui_disabled
} }
fn verifier_settings(&self) -> VerifierSettings {
let mut settings = VerifierSettings::default();
settings.scale_verifiers = self.args.flag_scale_verifiers;
if let Some(num_verifiers) = self.args.flag_num_verifiers {
settings.num_verifiers = num_verifiers;
}
settings
}
} }
#[cfg(test)] #[cfg(test)]
@ -803,6 +818,7 @@ mod tests {
vm_type: VMType::Interpreter, vm_type: VMType::Interpreter,
check_seal: true, check_seal: true,
with_color: !cfg!(windows), with_color: !cfg!(windows),
verifier_settings: Default::default(),
}))); })));
} }
@ -926,6 +942,7 @@ mod tests {
no_periodic_snapshot: false, no_periodic_snapshot: false,
check_seal: true, check_seal: true,
download_old_blocks: true, download_old_blocks: true,
verifier_settings: Default::default(),
})); }));
} }

View File

@ -28,6 +28,7 @@ use ethcore::service::ClientService;
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::AccountProvider;
use ethcore::miner::{Miner, MinerService, ExternalMiner, MinerOptions}; use ethcore::miner::{Miner, MinerService, ExternalMiner, MinerOptions};
use ethcore::snapshot; use ethcore::snapshot;
use ethcore::verification::queue::VerifierSettings;
use ethsync::SyncConfig; use ethsync::SyncConfig;
use informant::Informant; use informant::Informant;
@ -92,6 +93,7 @@ pub struct RunCmd {
pub no_periodic_snapshot: bool, pub no_periodic_snapshot: bool,
pub check_seal: bool, pub check_seal: bool,
pub download_old_blocks: bool, pub download_old_blocks: bool,
pub verifier_settings: VerifierSettings,
} }
pub fn open_ui(dapps_conf: &dapps::Configuration, signer_conf: &signer::Configuration) -> Result<(), String> { pub fn open_ui(dapps_conf: &dapps::Configuration, signer_conf: &signer::Configuration) -> Result<(), String> {
@ -231,7 +233,7 @@ pub fn execute(cmd: RunCmd, logger: Arc<RotatingLogger>) -> Result<(), String> {
} }
// create client config // create client config
let client_config = to_client_config( let mut client_config = to_client_config(
&cmd.cache_config, &cmd.cache_config,
mode.clone(), mode.clone(),
tracing, tracing,
@ -245,6 +247,8 @@ pub fn execute(cmd: RunCmd, logger: Arc<RotatingLogger>) -> Result<(), String> {
cmd.check_seal, cmd.check_seal,
); );
client_config.queue.verifier_settings = cmd.verifier_settings;
// set up bootnodes // set up bootnodes
let mut net_conf = cmd.net_conf; let mut net_conf = cmd.net_conf;
if !cmd.custom_bootnodes { if !cmd.custom_bootnodes {

View File

@ -494,7 +494,7 @@ fn rpc_eth_pending_transaction_by_hash() {
tester.miner.pending_transactions.lock().insert(H256::zero(), tx); tester.miner.pending_transactions.lock().insert(H256::zero(), tx);
} }
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":null,"blockNumber":null,"creates":null,"from":"0x0f65fe9276bc9a24ae7083ae28e2660ef72df99e","gas":"0x5208","gasPrice":"0x1","hash":"0x41df922fd0d4766fcc02e161f8295ec28522f329ae487f14d811e4b64c8d6e31","input":"0x","nonce":"0x0","publicKey":"0x7ae46da747962c2ee46825839c1ef9298e3bd2e70ca2938495c3693a485ec3eaa8f196327881090ff64cf4fbb0a48485d4f83098e189ed3b7a87d5941b59f789","r":"0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353","raw":"0xf85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","s":"0xefffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","to":"0x095e7baea6a6c7c4c2dfeb977efac326af552d87","transactionIndex":null,"v":0,"value":"0xa"},"id":1}"#; let response = r#"{"jsonrpc":"2.0","result":{"blockHash":null,"blockNumber":null,"creates":null,"from":"0x0f65fe9276bc9a24ae7083ae28e2660ef72df99e","gas":"0x5208","gasPrice":"0x1","hash":"0x41df922fd0d4766fcc02e161f8295ec28522f329ae487f14d811e4b64c8d6e31","input":"0x","networkId":null,"nonce":"0x0","publicKey":"0x7ae46da747962c2ee46825839c1ef9298e3bd2e70ca2938495c3693a485ec3eaa8f196327881090ff64cf4fbb0a48485d4f83098e189ed3b7a87d5941b59f789","r":"0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353","raw":"0xf85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","s":"0xefffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","standardV":"0x0","to":"0x095e7baea6a6c7c4c2dfeb977efac326af552d87","transactionIndex":null,"v":"0x1b","value":"0xa"},"id":1}"#;
let request = r#"{ let request = r#"{
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "eth_getTransactionByHash", "method": "eth_getTransactionByHash",
@ -809,13 +809,16 @@ fn rpc_eth_sign_transaction() {
&format!("\"from\":\"0x{:?}\",", &address) + &format!("\"from\":\"0x{:?}\",", &address) +
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# + r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
&format!("\"hash\":\"0x{:?}\",", t.hash()) + &format!("\"hash\":\"0x{:?}\",", t.hash()) +
r#""input":"0x","nonce":"0x1","# + r#""input":"0x","# +
&format!("\"networkId\":{},", t.network_id().map_or("null".to_owned(), |n| format!("{}", n))) +
r#""nonce":"0x1","# +
&format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) + &format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) +
&format!("\"r\":\"0x{}\",", signature.r().to_hex()) + &format!("\"r\":\"0x{}\",", U256::from(signature.r()).to_hex()) +
&format!("\"raw\":\"0x{}\",", rlp.to_hex()) + &format!("\"raw\":\"0x{}\",", rlp.to_hex()) +
&format!("\"s\":\"0x{}\",", signature.s().to_hex()) + &format!("\"s\":\"0x{}\",", U256::from(signature.s()).to_hex()) +
&format!("\"standardV\":\"0x{}\",", U256::from(t.standard_v()).to_hex()) +
r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# + r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# +
&format!("\"v\":{},", signature.v()) + &format!("\"v\":\"0x{}\",", U256::from(t.original_v()).to_hex()) +
r#""value":"0x9184e72a""# + r#""value":"0x9184e72a""# +
r#"}},"id":1}"#; r#"}},"id":1}"#;

View File

@ -284,13 +284,16 @@ fn should_add_sign_transaction_to_the_queue() {
&format!("\"from\":\"0x{:?}\",", &address) + &format!("\"from\":\"0x{:?}\",", &address) +
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# + r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
&format!("\"hash\":\"0x{:?}\",", t.hash()) + &format!("\"hash\":\"0x{:?}\",", t.hash()) +
r#""input":"0x","nonce":"0x1","# + r#""input":"0x","# +
&format!("\"networkId\":{},", t.network_id().map_or("null".to_owned(), |n| format!("{}", n))) +
r#""nonce":"0x1","# +
&format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) + &format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) +
&format!("\"r\":\"0x{}\",", signature.r().to_hex()) + &format!("\"r\":\"0x{}\",", U256::from(signature.r()).to_hex()) +
&format!("\"raw\":\"0x{}\",", rlp.to_hex()) + &format!("\"raw\":\"0x{}\",", rlp.to_hex()) +
&format!("\"s\":\"0x{}\",", signature.s().to_hex()) + &format!("\"s\":\"0x{}\",", U256::from(signature.s()).to_hex()) +
&format!("\"standardV\":\"0x{}\",", U256::from(t.standard_v()).to_hex()) +
r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# + r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# +
&format!("\"v\":{},", signature.v()) + &format!("\"v\":\"0x{}\",", U256::from(t.original_v()).to_hex()) +
r#""value":"0x9184e72a""# + r#""value":"0x9184e72a""# +
r#"}},"id":1}"#; r#"}},"id":1}"#;

View File

@ -139,7 +139,7 @@ mod tests {
fn test_serialize_block_transactions() { fn test_serialize_block_transactions() {
let t = BlockTransactions::Full(vec![Transaction::default()]); let t = BlockTransactions::Full(vec![Transaction::default()]);
let serialized = serde_json::to_string(&t).unwrap(); let serialized = serde_json::to_string(&t).unwrap();
assert_eq!(serialized, r#"[{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"v":0,"r":"0x0000000000000000000000000000000000000000000000000000000000000000","s":"0x0000000000000000000000000000000000000000000000000000000000000000"}]"#); assert_eq!(serialized, r#"[{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0"}]"#);
let t = BlockTransactions::Hashes(vec![H256::default().into()]); let t = BlockTransactions::Hashes(vec![H256::default().into()]);
let serialized = serde_json::to_string(&t).unwrap(); let serialized = serde_json::to_string(&t).unwrap();

View File

@ -57,12 +57,18 @@ pub struct Transaction {
/// Public key of the signer. /// Public key of the signer.
#[serde(rename="publicKey")] #[serde(rename="publicKey")]
pub public_key: Option<H512>, pub public_key: Option<H512>,
/// The V field of the signature. /// The network id of the transaction, if any.
pub v: u8, #[serde(rename="networkId")]
pub network_id: Option<u64>,
/// The standardised V field of the signature (0 or 1).
#[serde(rename="standardV")]
pub standard_v: U256,
/// The standardised V field of the signature.
pub v: U256,
/// The R field of the signature. /// The R field of the signature.
pub r: H256, pub r: U256,
/// The S field of the signature. /// The S field of the signature.
pub s: H256, pub s: U256,
} }
/// Local Transaction Status /// Local Transaction Status
@ -176,7 +182,9 @@ impl From<LocalizedTransaction> for Transaction {
}, },
raw: ::rlp::encode(&t.signed).to_vec().into(), raw: ::rlp::encode(&t.signed).to_vec().into(),
public_key: t.public_key().ok().map(Into::into), public_key: t.public_key().ok().map(Into::into),
v: signature.v(), network_id: t.network_id(),
standard_v: t.standard_v().into(),
v: t.original_v().into(),
r: signature.r().into(), r: signature.r().into(),
s: signature.s().into(), s: signature.s().into(),
} }
@ -207,7 +215,9 @@ impl From<SignedTransaction> for Transaction {
}, },
raw: ::rlp::encode(&t).to_vec().into(), raw: ::rlp::encode(&t).to_vec().into(),
public_key: t.public_key().ok().map(Into::into), public_key: t.public_key().ok().map(Into::into),
v: signature.v(), network_id: t.network_id(),
standard_v: t.standard_v().into(),
v: t.original_v().into(),
r: signature.r().into(), r: signature.r().into(),
s: signature.s().into(), s: signature.s().into(),
} }
@ -238,7 +248,7 @@ mod tests {
fn test_transaction_serialize() { fn test_transaction_serialize() {
let t = Transaction::default(); let t = Transaction::default();
let serialized = serde_json::to_string(&t).unwrap(); let serialized = serde_json::to_string(&t).unwrap();
assert_eq!(serialized, r#"{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"v":0,"r":"0x0000000000000000000000000000000000000000000000000000000000000000","s":"0x0000000000000000000000000000000000000000000000000000000000000000"}"#); assert_eq!(serialized, r#"{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0"}"#);
} }
#[test] #[test]

View File

@ -14,9 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::cmp;
use std::str::FromStr; use std::str::FromStr;
use rustc_serialize::hex::ToHex;
use serde; use serde;
use util::{U256 as EthU256, U128 as EthU128, Uint}; use util::{U256 as EthU256, U128 as EthU128, Uint};
@ -50,18 +48,7 @@ macro_rules! impl_uint {
impl serde::Serialize for $name { impl serde::Serialize for $name {
fn serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error> where S: serde::Serializer { fn serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error> where S: serde::Serializer {
let mut hex = "0x".to_owned(); serializer.serialize_str(&format!("0x{}", self.0.to_hex()))
let mut bytes = [0u8; 8 * $size];
self.0.to_big_endian(&mut bytes);
let len = cmp::max((self.0.bits() + 7) / 8, 1);
let bytes_hex = bytes[bytes.len() - len..].to_hex();
if bytes_hex.starts_with('0') {
hex.push_str(&bytes_hex[1..]);
} else {
hex.push_str(&bytes_hex);
}
serializer.serialize_str(&hex)
} }
} }

View File

@ -37,12 +37,12 @@
//! implementations for even more speed, hidden behind the `x64_arithmetic` //! implementations for even more speed, hidden behind the `x64_arithmetic`
//! feature flag. //! feature flag.
use std::{mem, fmt}; use std::{mem, fmt, cmp};
use std::str::{FromStr}; use std::str::{FromStr};
use std::hash::Hash; use std::hash::Hash;
use std::ops::{Shr, Shl, BitAnd, BitOr, BitXor, Not, Div, Rem, Mul, Add, Sub}; use std::ops::{Shr, Shl, BitAnd, BitOr, BitXor, Not, Div, Rem, Mul, Add, Sub};
use std::cmp::Ordering; use std::cmp::Ordering;
use rustc_serialize::hex::{FromHex, FromHexError}; use rustc_serialize::hex::{ToHex, FromHex, FromHexError};
/// Conversion from decimal string error /// Conversion from decimal string error
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -520,8 +520,10 @@ pub trait Uint: Sized + Default + FromStr + From<u64> + fmt::Debug + fmt::Displa
fn bit(&self, index: usize) -> bool; fn bit(&self, index: usize) -> bool;
/// Return single byte /// Return single byte
fn byte(&self, index: usize) -> u8; fn byte(&self, index: usize) -> u8;
/// Convert U256 to the sequence of bytes with a big endian /// Convert to the sequence of bytes with a big endian
fn to_big_endian(&self, bytes: &mut[u8]); fn to_big_endian(&self, bytes: &mut[u8]);
/// Convert to a non-zero-prefixed hex representation (not prefixed by `0x`).
fn to_hex(&self) -> String;
/// Create `Uint(10**n)` /// Create `Uint(10**n)`
fn exp10(n: usize) -> Self; fn exp10(n: usize) -> Self;
/// Return eponentation `self**other`. Panic on overflow. /// Return eponentation `self**other`. Panic on overflow.
@ -684,6 +686,17 @@ macro_rules! construct_uint {
} }
} }
#[inline]
fn to_hex(&self) -> String {
if self.is_zero() { return "0".to_owned(); } // special case.
let mut bytes = [0u8; 8 * $n_words];
self.to_big_endian(&mut bytes);
let bp7 = self.bits() + 7;
let len = cmp::max(bp7 / 8, 1);
let bytes_hex = bytes[bytes.len() - len..].to_hex();
(&bytes_hex[1 - bp7 % 8 / 4..]).to_owned()
}
#[inline] #[inline]
fn exp10(n: usize) -> Self { fn exp10(n: usize) -> Self {
match n { match n {
@ -1637,7 +1650,7 @@ mod tests {
} }
#[test] #[test]
fn uint256_pow () { fn uint256_pow() {
assert_eq!(U256::from(10).pow(U256::from(0)), U256::from(1)); assert_eq!(U256::from(10).pow(U256::from(0)), U256::from(1));
assert_eq!(U256::from(10).pow(U256::from(1)), U256::from(10)); assert_eq!(U256::from(10).pow(U256::from(1)), U256::from(10));
assert_eq!(U256::from(10).pow(U256::from(2)), U256::from(100)); assert_eq!(U256::from(10).pow(U256::from(2)), U256::from(100));
@ -1647,12 +1660,24 @@ mod tests {
#[test] #[test]
#[should_panic] #[should_panic]
fn uint256_pow_overflow_panic () { fn uint256_pow_overflow_panic() {
U256::from(2).pow(U256::from(0x100)); U256::from(2).pow(U256::from(0x100));
} }
#[test] #[test]
fn uint256_overflowing_pow () { fn should_format_hex_correctly() {
assert_eq!(&U256::from(0).to_hex(), &"0");
assert_eq!(&U256::from(0x1).to_hex(), &"1");
assert_eq!(&U256::from(0xf).to_hex(), &"f");
assert_eq!(&U256::from(0x10).to_hex(), &"10");
assert_eq!(&U256::from(0xff).to_hex(), &"ff");
assert_eq!(&U256::from(0x100).to_hex(), &"100");
assert_eq!(&U256::from(0xfff).to_hex(), &"fff");
assert_eq!(&U256::from(0x1000).to_hex(), &"1000");
}
#[test]
fn uint256_overflowing_pow() {
// assert_eq!( // assert_eq!(
// U256::from(2).overflowing_pow(U256::from(0xff)), // U256::from(2).overflowing_pow(U256::from(0xff)),
// (U256::from_str("8000000000000000000000000000000000000000000000000000000000000000").unwrap(), false) // (U256::from_str("8000000000000000000000000000000000000000000000000000000000000000").unwrap(), false)