Merge branch 'master' into transactions-propagate
This commit is contained in:
commit
0848a9ec3c
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1250,7 +1250,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parity-ui-precompiled"
|
||||
version = "1.4.0"
|
||||
source = "git+https://github.com/ethcore/js-precompiled.git#957c5a66c33f3b06a7ae804ac5edc59c20e4535b"
|
||||
source = "git+https://github.com/ethcore/js-precompiled.git#427319583ccde288ba26728c14384392ddbba93d"
|
||||
dependencies = [
|
||||
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
@ -182,8 +182,8 @@
|
||||
"enode://89d5dc2a81e574c19d0465f497c1af96732d1b61a41de89c2a37f35707689ac416529fae1038809852b235c2d30fd325abdc57c122feeefbeaaf802cc7e9580d@45.55.33.62:30303",
|
||||
"enode://605e04a43b1156966b3a3b66b980c87b7f18522f7f712035f84576016be909a2798a438b2b17b1a8c58db314d88539a77419ca4be36148c086900fba487c9d39@188.166.255.12:30303",
|
||||
"enode://016b20125f447a3b203a3cae953b2ede8ffe51290c071e7599294be84317635730c397b8ff74404d6be412d539ee5bb5c3c700618723d3b53958c92bd33eaa82@159.203.210.80:30303",
|
||||
"enode://01f76fa0561eca2b9a7e224378dd854278735f1449793c46ad0c4e79e8775d080c21dcc455be391e90a98153c3b05dcc8935c8440de7b56fe6d67251e33f4e3c@10.6.6.117:30303",
|
||||
"enode://fe11ef89fc5ac9da358fc160857855f25bbf9e332c79b9ca7089330c02b728b2349988c6062f10982041702110745e203d26975a6b34bcc97144f9fe439034e8@10.1.72.117:30303"
|
||||
"enode://01f76fa0561eca2b9a7e224378dd854278735f1449793c46ad0c4e79e8775d080c21dcc455be391e90a98153c3b05dcc8935c8440de7b56fe6d67251e33f4e3c@51.15.42.252:30303",
|
||||
"enode://8d91c8137890d29110b9463882f17ae4e279cd2c90cf56573187ed1c8546fca5f590a9f05e9f108eb1bd91767ed01ede4daad9e001b61727885eaa246ddb39c2@163.172.171.38:30303"
|
||||
],
|
||||
"accounts": {
|
||||
"0000000000000000000000000000000000000001": { "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
||||
|
@ -21,6 +21,7 @@ use util::*;
|
||||
use util::using_queue::{UsingQueue, GetAction};
|
||||
use account_provider::AccountProvider;
|
||||
use views::{BlockView, HeaderView};
|
||||
use header::Header;
|
||||
use state::{State, CleanupMode};
|
||||
use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockID, CallAnalytics};
|
||||
use client::TransactionImportResult;
|
||||
@ -577,7 +578,16 @@ impl Miner {
|
||||
|
||||
let schedule = chain.latest_schedule();
|
||||
let gas_required = |tx: &SignedTransaction| tx.gas_required(&schedule).into();
|
||||
let best_block_header: Header = ::rlp::decode(&chain.best_block_header());
|
||||
transactions.into_iter()
|
||||
.filter(|tx| match self.engine.verify_transaction_basic(tx, &best_block_header) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
debug!(target: "miner", "Rejected tx {:?} with invalid signature: {:?}", tx.hash(), e);
|
||||
false
|
||||
}
|
||||
}
|
||||
)
|
||||
.map(|tx| {
|
||||
let origin = accounts.as_ref().and_then(|accounts| {
|
||||
tx.sender().ok().and_then(|sender| match accounts.contains(&sender) {
|
||||
|
@ -34,16 +34,43 @@ pub const KEY_LENGTH: usize = 32;
|
||||
pub const KEY_ITERATIONS: usize = 10240;
|
||||
pub const KEY_LENGTH_AES: usize = KEY_LENGTH / 2;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum ScryptError {
|
||||
// log(N) < r / 16
|
||||
InvalidN,
|
||||
// p <= (2^31-1 * 32)/(128 * r)
|
||||
InvalidP,
|
||||
}
|
||||
|
||||
impl fmt::Display for ScryptError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
let s = match *self {
|
||||
ScryptError::InvalidN => "Invalid N argument of the scrypt encryption" ,
|
||||
ScryptError::InvalidP => "Invalid p argument of the scrypt encryption",
|
||||
};
|
||||
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Error {
|
||||
Secp(SecpError),
|
||||
Scrypt(ScryptError),
|
||||
InvalidMessage,
|
||||
}
|
||||
|
||||
impl From<ScryptError> for Error {
|
||||
fn from(err: ScryptError) -> Self {
|
||||
Error::Scrypt(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
let s = match *self {
|
||||
Error::Secp(ref err) => err.to_string(),
|
||||
Error::Scrypt(ref err) => err.to_string(),
|
||||
Error::InvalidMessage => "Invalid message".into(),
|
||||
};
|
||||
|
||||
@ -80,13 +107,23 @@ pub fn derive_key_iterations(password: &str, salt: &[u8; 32], c: u32) -> (Vec<u8
|
||||
(derived_right_bits.to_vec(), derived_left_bits.to_vec())
|
||||
}
|
||||
|
||||
pub fn derive_key_scrypt(password: &str, salt: &[u8; 32], n: u32, p: u32, r: u32) -> (Vec<u8>, Vec<u8>) {
|
||||
pub fn derive_key_scrypt(password: &str, salt: &[u8; 32], n: u32, p: u32, r: u32) -> Result<(Vec<u8>, Vec<u8>), Error> {
|
||||
// sanity checks
|
||||
let log_n = (32 - n.leading_zeros() - 1) as u8;
|
||||
if log_n as u32 >= r * 16 {
|
||||
return Err(Error::Scrypt(ScryptError::InvalidN));
|
||||
}
|
||||
|
||||
if p as u64 > ((u32::max_value() as u64 - 1) * 32)/(128 * (r as u64)) {
|
||||
return Err(Error::Scrypt(ScryptError::InvalidP));
|
||||
}
|
||||
|
||||
let mut derived_key = vec![0u8; KEY_LENGTH];
|
||||
let scrypt_params = ScryptParams::new(n.trailing_zeros() as u8, r, p);
|
||||
let scrypt_params = ScryptParams::new(log_n, r, p);
|
||||
scrypt(password.as_bytes(), salt, &scrypt_params, &mut derived_key);
|
||||
let derived_right_bits = &derived_key[0..KEY_LENGTH_AES];
|
||||
let derived_left_bits = &derived_key[KEY_LENGTH_AES..KEY_LENGTH];
|
||||
(derived_right_bits.to_vec(), derived_left_bits.to_vec())
|
||||
Ok((derived_right_bits.to_vec(), derived_left_bits.to_vec()))
|
||||
}
|
||||
|
||||
pub fn derive_mac(derived_left_bits: &[u8], cipher_text: &[u8]) -> Vec<u8> {
|
||||
|
@ -113,7 +113,7 @@ impl Crypto {
|
||||
|
||||
let (derived_left_bits, derived_right_bits) = match self.kdf {
|
||||
Kdf::Pbkdf2(ref params) => crypto::derive_key_iterations(password, ¶ms.salt, params.c),
|
||||
Kdf::Scrypt(ref params) => crypto::derive_key_scrypt(password, ¶ms.salt, params.n, params.p, params.r),
|
||||
Kdf::Scrypt(ref params) => try!(crypto::derive_key_scrypt(password, ¶ms.salt, params.n, params.p, params.r)),
|
||||
};
|
||||
|
||||
let mac = crypto::derive_mac(&derived_right_bits, &self.ciphertext).keccak256();
|
||||
|
@ -86,7 +86,7 @@ impl SecretStore for EthStore {
|
||||
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
|
||||
let keypair = try!(KeyPair::from_secret(secret).map_err(|_| Error::CreationFailed));
|
||||
let id: [u8; 16] = Random::random();
|
||||
let account = SafeAccount::create(&keypair, id, password, self.iterations, UUID::from(id).into(), "{}".to_owned());
|
||||
let account = SafeAccount::create(&keypair, id, password, self.iterations, "".to_owned(), "{}".to_owned());
|
||||
let address = account.address.clone();
|
||||
try!(self.save(account));
|
||||
Ok(address)
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parity.js",
|
||||
"version": "0.2.50",
|
||||
"version": "0.2.54",
|
||||
"main": "release/index.js",
|
||||
"jsnext:main": "src/index.js",
|
||||
"author": "Parity Team <admin@parity.io>",
|
||||
@ -43,6 +43,7 @@
|
||||
"test": "mocha 'src/**/*.spec.js'",
|
||||
"test:coverage": "istanbul cover _mocha -- 'src/**/*.spec.js'",
|
||||
"test:e2e": "mocha 'src/**/*.e2e.js'",
|
||||
"test:npm": "(cd .npmjs && npm i) && node test/npmLibrary && (rm -rf .npmjs/node_modules)",
|
||||
"prepush": "npm run lint:cached"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -101,9 +102,10 @@
|
||||
"postcss-nested": "^1.0.0",
|
||||
"postcss-simple-vars": "^3.0.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react-addons-test-utils": "^15.3.0",
|
||||
"react-addons-test-utils": "~15.3.2",
|
||||
"react-copy-to-clipboard": "^4.2.3",
|
||||
"react-hot-loader": "^1.3.0",
|
||||
"react-dom": "~15.3.2",
|
||||
"react-hot-loader": "~1.3.0",
|
||||
"rucksack-css": "^0.8.6",
|
||||
"sinon": "^1.17.4",
|
||||
"sinon-as-promised": "^4.0.2",
|
||||
@ -113,7 +115,7 @@
|
||||
"webpack": "^1.13.2",
|
||||
"webpack-dev-server": "^1.15.2",
|
||||
"webpack-error-notification": "0.1.6",
|
||||
"webpack-hot-middleware": "^2.7.1",
|
||||
"webpack-hot-middleware": "~2.13.2",
|
||||
"websocket": "^1.0.23"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -133,23 +135,24 @@
|
||||
"js-sha3": "^0.5.2",
|
||||
"lodash": "^4.11.1",
|
||||
"marked": "^0.3.6",
|
||||
"material-ui": "^0.16.1",
|
||||
"material-ui": "0.16.1",
|
||||
"material-ui-chip-input": "^0.8.0",
|
||||
"mobx": "^2.6.1",
|
||||
"mobx-react": "^3.5.8",
|
||||
"mobx-react-devtools": "^4.2.9",
|
||||
"moment": "^2.14.1",
|
||||
"phoneformat.js": "^1.0.3",
|
||||
"qs": "^6.3.0",
|
||||
"react": "^15.2.1",
|
||||
"react": "~15.3.2",
|
||||
"react-ace": "^4.0.0",
|
||||
"react-addons-css-transition-group": "^15.2.1",
|
||||
"react-addons-css-transition-group": "~15.3.2",
|
||||
"react-chartjs-2": "^1.5.0",
|
||||
"react-dom": "^15.2.1",
|
||||
"react-dom": "~15.3.2",
|
||||
"react-dropzone": "^3.7.3",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^2.6.1",
|
||||
"react-router-redux": "^4.0.5",
|
||||
"react-tap-event-plugin": "^1.0.0",
|
||||
"react-tap-event-plugin": "~1.0.0",
|
||||
"react-tooltip": "^2.0.3",
|
||||
"recharts": "^0.15.2",
|
||||
"redux": "^3.5.2",
|
||||
|
@ -27,6 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bignumber.js": "^2.3.0",
|
||||
"js-sha3": "^0.5.2"
|
||||
"js-sha3": "^0.5.2",
|
||||
"node-fetch": "^1.6.3"
|
||||
}
|
||||
}
|
||||
|
@ -166,3 +166,11 @@ export function inTraceFilter (filterObject) {
|
||||
|
||||
return filterObject;
|
||||
}
|
||||
|
||||
export function inTraceType (whatTrace) {
|
||||
if (isString(whatTrace)) {
|
||||
return [whatTrace];
|
||||
}
|
||||
|
||||
return whatTrace;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
import { inAddress, inBlockNumber, inData, inFilter, inHex, inNumber10, inNumber16, inOptions } from './input';
|
||||
import { inAddress, inBlockNumber, inData, inFilter, inHex, inNumber10, inNumber16, inOptions, inTraceType } from './input';
|
||||
import { isAddress } from '../../../test/types';
|
||||
|
||||
describe('api/format/input', () => {
|
||||
@ -242,4 +242,16 @@ describe('api/format/input', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('inTraceType', () => {
|
||||
it('returns array of types as is', () => {
|
||||
const types = ['vmTrace', 'trace', 'stateDiff'];
|
||||
expect(inTraceType(types)).to.deep.equal(types);
|
||||
});
|
||||
|
||||
it('formats single string type into array', () => {
|
||||
const type = 'vmTrace';
|
||||
expect(inTraceType(type)).to.deep.equal([type]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -254,3 +254,25 @@ export function outTrace (trace) {
|
||||
|
||||
return trace;
|
||||
}
|
||||
|
||||
export function outTraces (traces) {
|
||||
if (traces) {
|
||||
return traces.map(outTrace);
|
||||
}
|
||||
|
||||
return traces;
|
||||
}
|
||||
|
||||
export function outTraceReplay (trace) {
|
||||
if (trace) {
|
||||
Object.keys(trace).forEach((key) => {
|
||||
switch (key) {
|
||||
case 'trace':
|
||||
trace[key] = outTraces(trace[key]);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return trace;
|
||||
}
|
||||
|
@ -20,15 +20,25 @@ describe('ethapi.trace', () => {
|
||||
const ethapi = createHttpApi();
|
||||
|
||||
describe('block', () => {
|
||||
it('returns the latest block', () => {
|
||||
return ethapi.trace.block().then((block) => {
|
||||
expect(block).to.be.ok;
|
||||
it('returns the latest block traces', () => {
|
||||
return ethapi.trace.block().then((traces) => {
|
||||
expect(traces).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a specified block', () => {
|
||||
return ethapi.trace.block('0x65432').then((block) => {
|
||||
expect(block).to.be.ok;
|
||||
it('returns traces for a specified block', () => {
|
||||
return ethapi.trace.block('0x65432').then((traces) => {
|
||||
expect(traces).to.be.ok;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('replayTransaction', () => {
|
||||
it('returns traces for a specific transaction', () => {
|
||||
return ethapi.eth.getBlockByNumber().then((latestBlock) => {
|
||||
return ethapi.trace.replayTransaction(latestBlock.transactions[0]).then((traces) => {
|
||||
expect(traces).to.be.ok;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -14,35 +14,53 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { inBlockNumber, inHex, inNumber16, inTraceFilter } from '../../format/input';
|
||||
import { outTrace } from '../../format/output';
|
||||
import { inBlockNumber, inData, inHex, inNumber16, inOptions, inTraceFilter, inTraceType } from '../../format/input';
|
||||
import { outTraces, outTraceReplay } from '../../format/output';
|
||||
|
||||
export default class Trace {
|
||||
constructor (transport) {
|
||||
this._transport = transport;
|
||||
}
|
||||
|
||||
block (blockNumber = 'latest') {
|
||||
return this._transport
|
||||
.execute('trace_block', inBlockNumber(blockNumber))
|
||||
.then(outTraces);
|
||||
}
|
||||
|
||||
call (options, blockNumber = 'latest', whatTrace = ['trace']) {
|
||||
return this._transport
|
||||
.execute('trace_call', inOptions(options), inBlockNumber(blockNumber), inTraceType(whatTrace))
|
||||
.then(outTraceReplay);
|
||||
}
|
||||
|
||||
filter (filterObj) {
|
||||
return this._transport
|
||||
.execute('trace_filter', inTraceFilter(filterObj))
|
||||
.then(traces => traces.map(trace => outTrace(trace)));
|
||||
.then(outTraces);
|
||||
}
|
||||
|
||||
get (txHash, position) {
|
||||
return this._transport
|
||||
.execute('trace_get', inHex(txHash), inNumber16(position))
|
||||
.then(trace => outTrace(trace));
|
||||
.then(outTraces);
|
||||
}
|
||||
|
||||
rawTransaction (data, whatTrace = ['trace']) {
|
||||
return this._transport
|
||||
.execute('trace_rawTransaction', inData(data), inTraceType(whatTrace))
|
||||
.then(outTraceReplay);
|
||||
}
|
||||
|
||||
replayTransaction (txHash, whatTrace = ['trace']) {
|
||||
return this._transport
|
||||
.execute('trace_replayTransaction', txHash, inTraceType(whatTrace))
|
||||
.then(outTraceReplay);
|
||||
}
|
||||
|
||||
transaction (txHash) {
|
||||
return this._transport
|
||||
.execute('trace_transaction', inHex(txHash))
|
||||
.then(traces => traces.map(trace => outTrace(trace)));
|
||||
}
|
||||
|
||||
block (blockNumber = 'latest') {
|
||||
return this._transport
|
||||
.execute('trace_block', inBlockNumber(blockNumber))
|
||||
.then(traces => traces.map(trace => outTrace(trace)));
|
||||
.then(outTraces);
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ export default class Ws extends JsonRpcBase {
|
||||
this._connecting = false;
|
||||
|
||||
if (this._autoConnect) {
|
||||
this._connect();
|
||||
setTimeout(() => this._connect(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import githubhint from './githubhint.json';
|
||||
import owned from './owned.json';
|
||||
import registry from './registry.json';
|
||||
import signaturereg from './signaturereg.json';
|
||||
import smsverification from './sms-verification.json';
|
||||
import tokenreg from './tokenreg.json';
|
||||
import wallet from './wallet.json';
|
||||
|
||||
@ -36,6 +37,7 @@ export {
|
||||
owned,
|
||||
registry,
|
||||
signaturereg,
|
||||
smsverification,
|
||||
tokenreg,
|
||||
wallet
|
||||
};
|
||||
|
1
js/src/contracts/abi/sms-verification.json
Normal file
1
js/src/contracts/abi/sms-verification.json
Normal file
@ -0,0 +1 @@
|
||||
[{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"certify","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"request","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"},{"name":"_puzzle","type":"bytes32"}],"name":"puzzle","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"revoke","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes32"}],"name":"confirm","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"delegate","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setDelegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"}],"name":"certified","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Requested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"puzzle","type":"bytes32"}],"name":"Puzzled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Confirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Revoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}]
|
@ -19,6 +19,7 @@ import Registry from './registry';
|
||||
import SignatureReg from './signaturereg';
|
||||
import TokenReg from './tokenreg';
|
||||
import GithubHint from './githubhint';
|
||||
import smsVerification from './sms-verification';
|
||||
|
||||
let instance = null;
|
||||
|
||||
@ -54,6 +55,10 @@ export default class Contracts {
|
||||
return this._githubhint;
|
||||
}
|
||||
|
||||
get smsVerification () {
|
||||
return smsVerification;
|
||||
}
|
||||
|
||||
static create (api) {
|
||||
return new Contracts(api);
|
||||
}
|
||||
|
52
js/src/contracts/sms-verification.js
Normal file
52
js/src/contracts/sms-verification.js
Normal file
@ -0,0 +1,52 @@
|
||||
// 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 { stringify } from 'querystring';
|
||||
|
||||
export const checkIfVerified = (contract, account) => {
|
||||
return contract.instance.certified.call({}, [account]);
|
||||
};
|
||||
|
||||
export const checkIfRequested = (contract, account) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
contract.subscribe('Requested', {
|
||||
fromBlock: 0, toBlock: 'pending'
|
||||
}, (err, logs) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const e = logs.find((l) => {
|
||||
return l.type === 'mined' && l.params.who && l.params.who.value === account;
|
||||
});
|
||||
resolve(e ? e.transactionHash : false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const postToServer = (query) => {
|
||||
query = stringify(query);
|
||||
return fetch('https://sms-verification.parity.io/?' + query, {
|
||||
method: 'POST', mode: 'cors', cache: 'no-store'
|
||||
})
|
||||
.then((res) => {
|
||||
return res.json().then((data) => {
|
||||
if (res.ok) {
|
||||
return data.message;
|
||||
}
|
||||
throw new Error(data.message || 'unknown error');
|
||||
});
|
||||
});
|
||||
};
|
@ -15,12 +15,17 @@
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.container {
|
||||
.body {
|
||||
text-align: center;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
font-family: 'Roboto';
|
||||
vertical-align: middle;
|
||||
padding: 4em 0;
|
||||
text-align: center;
|
||||
margin: 0 0 2em 0;
|
||||
}
|
||||
|
||||
.form {
|
||||
@ -98,7 +103,7 @@
|
||||
color: #333;
|
||||
background: #eee;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
border-radius: 0.5em;
|
||||
width: 100%;
|
||||
font-size: 1em;
|
||||
text-align: center;
|
||||
@ -113,20 +118,29 @@
|
||||
}
|
||||
|
||||
.hashError, .hashWarning, .hashOk {
|
||||
padding-top: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
text-align: center;
|
||||
padding: 1em 0;
|
||||
border: 0.25em solid #333;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.hashError {
|
||||
border-color: #f66;
|
||||
color: #f66;
|
||||
background: rgba(255, 102, 102, 0.25);
|
||||
}
|
||||
|
||||
.hashWarning {
|
||||
border-color: #f80;
|
||||
color: #f80;
|
||||
background: rgba(255, 236, 0, 0.25);
|
||||
}
|
||||
|
||||
.hashOk {
|
||||
opacity: 0.5;
|
||||
border-color: #6f6;
|
||||
color: #6f6;
|
||||
background: rgba(102, 255, 102, 0.25);
|
||||
}
|
||||
|
||||
.typeButtons {
|
||||
|
@ -19,6 +19,7 @@ import React, { Component } from 'react';
|
||||
import { api } from '../parity';
|
||||
import { attachInterface } from '../services';
|
||||
import Button from '../Button';
|
||||
import Events from '../Events';
|
||||
import IdentityIcon from '../IdentityIcon';
|
||||
import Loading from '../Loading';
|
||||
|
||||
@ -27,6 +28,8 @@ import styles from './application.css';
|
||||
const INVALID_URL_HASH = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
|
||||
let nextEventId = 0;
|
||||
|
||||
export default class Application extends Component {
|
||||
state = {
|
||||
fromAddress: null,
|
||||
@ -43,7 +46,9 @@ export default class Application extends Component {
|
||||
registerState: '',
|
||||
registerType: 'file',
|
||||
repo: '',
|
||||
repoError: null
|
||||
repoError: null,
|
||||
events: {},
|
||||
eventIds: []
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@ -75,7 +80,7 @@ export default class Application extends Component {
|
||||
let hashClass = null;
|
||||
if (contentHashError) {
|
||||
hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning;
|
||||
} else {
|
||||
} else if (contentHash) {
|
||||
hashClass = styles.hashOk;
|
||||
}
|
||||
|
||||
@ -116,29 +121,34 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<div className={ styles.form }>
|
||||
<div className={ styles.typeButtons }>
|
||||
<Button
|
||||
disabled={ registerBusy }
|
||||
invert={ registerType !== 'file' }
|
||||
onClick={ this.onClickTypeNormal }>File Link</Button>
|
||||
<Button
|
||||
disabled={ registerBusy }
|
||||
invert={ registerType !== 'content' }
|
||||
onClick={ this.onClickTypeContent }>Content Bundle</Button>
|
||||
</div>
|
||||
<div className={ styles.box }>
|
||||
<div className={ styles.description }>
|
||||
Provide a valid URL to register. The content information can be used in other contracts that allows for reverse lookups, e.g. image registries, dapp registries, etc.
|
||||
<div className={ styles.body }>
|
||||
<div className={ styles.container }>
|
||||
<div className={ styles.form }>
|
||||
<div className={ styles.typeButtons }>
|
||||
<Button
|
||||
disabled={ registerBusy }
|
||||
invert={ registerType !== 'file' }
|
||||
onClick={ this.onClickTypeNormal }>File Link</Button>
|
||||
<Button
|
||||
disabled={ registerBusy }
|
||||
invert={ registerType !== 'content' }
|
||||
onClick={ this.onClickTypeContent }>Content Bundle</Button>
|
||||
</div>
|
||||
{ valueInputs }
|
||||
<div className={ hashClass }>
|
||||
{ contentHashError || contentHash }
|
||||
<div className={ styles.box }>
|
||||
<div className={ styles.description }>
|
||||
Provide a valid URL to register. The content information can be used in other contracts that allows for reverse lookups, e.g. image registries, dapp registries, etc.
|
||||
</div>
|
||||
{ valueInputs }
|
||||
<div className={ hashClass }>
|
||||
{ contentHashError || contentHash }
|
||||
</div>
|
||||
{ registerBusy ? this.renderProgress() : this.renderButtons() }
|
||||
</div>
|
||||
{ registerBusy ? this.renderProgress() : this.renderButtons() }
|
||||
</div>
|
||||
</div>
|
||||
<Events
|
||||
eventIds={ this.state.eventIds }
|
||||
events={ this.state.events } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -285,15 +295,29 @@ export default class Application extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
trackRequest (promise) {
|
||||
trackRequest (eventId, promise) {
|
||||
return promise
|
||||
.then((signerRequestId) => {
|
||||
this.setState({ signerRequestId, registerState: 'Transaction posted, Waiting for transaction authorization' });
|
||||
this.setState({
|
||||
events: Object.assign({}, this.state.events, {
|
||||
[eventId]: Object.assign({}, this.state.events[eventId], {
|
||||
signerRequestId,
|
||||
registerState: 'Transaction posted, Waiting for transaction authorization'
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
return api.pollMethod('parity_checkRequest', signerRequestId);
|
||||
})
|
||||
.then((txHash) => {
|
||||
this.setState({ txHash, registerState: 'Transaction authorized, Waiting for network confirmations' });
|
||||
this.setState({
|
||||
events: Object.assign({}, this.state.events, {
|
||||
[eventId]: Object.assign({}, this.state.events[eventId], {
|
||||
txHash,
|
||||
registerState: 'Transaction authorized, Waiting for network confirmations'
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
return api.pollMethod('eth_getTransactionReceipt', txHash, (receipt) => {
|
||||
if (!receipt || !receipt.blockNumber || receipt.blockNumber.eq(0)) {
|
||||
@ -304,27 +328,72 @@ export default class Application extends Component {
|
||||
});
|
||||
})
|
||||
.then((txReceipt) => {
|
||||
this.setState({ txReceipt, registerBusy: false, registerState: 'Network confirmed, Received transaction receipt', url: '', commit: '', repo: '', commitError: null, contentHash: '', contentHashOwner: null, contentHashError: null });
|
||||
this.setState({
|
||||
events: Object.assign({}, this.state.events, {
|
||||
[eventId]: Object.assign({}, this.state.events[eventId], {
|
||||
txReceipt,
|
||||
registerBusy: false,
|
||||
registerState: 'Network confirmed, Received transaction receipt'
|
||||
})
|
||||
})
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('onSend', error);
|
||||
this.setState({ registerError: error.message });
|
||||
|
||||
this.setState({
|
||||
events: Object.assign({}, this.state.events, {
|
||||
[eventId]: Object.assign({}, this.state.events[eventId], {
|
||||
registerState: error.message,
|
||||
registerError: true,
|
||||
registerBusy: false
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
registerContent (repo, commit) {
|
||||
registerContent (contentRepo, contentCommit) {
|
||||
const { contentHash, fromAddress, instance } = this.state;
|
||||
contentCommit = contentCommit.substr(0, 2) === '0x' ? contentCommit : `0x${contentCommit}`;
|
||||
|
||||
this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' });
|
||||
|
||||
const values = [contentHash, repo, commit.substr(0, 2) === '0x' ? commit : `0x${commit}`];
|
||||
const eventId = nextEventId++;
|
||||
const values = [contentHash, contentRepo, contentCommit];
|
||||
const options = { from: fromAddress };
|
||||
|
||||
this.setState({
|
||||
eventIds: [eventId].concat(this.state.eventIds),
|
||||
events: Object.assign({}, this.state.events, {
|
||||
[eventId]: {
|
||||
contentHash,
|
||||
contentRepo,
|
||||
contentCommit,
|
||||
fromAddress,
|
||||
registerBusy: true,
|
||||
registerState: 'Estimating gas for the transaction',
|
||||
timestamp: new Date()
|
||||
}
|
||||
}),
|
||||
url: '',
|
||||
commit: '',
|
||||
repo: '',
|
||||
commitError: null,
|
||||
contentHash: '',
|
||||
contentHashOwner: null,
|
||||
contentHashError: null
|
||||
});
|
||||
|
||||
this.trackRequest(
|
||||
instance
|
||||
eventId, instance
|
||||
.hint.estimateGas(options, values)
|
||||
.then((gas) => {
|
||||
this.setState({ registerState: 'Gas estimated, Posting transaction to the network' });
|
||||
this.setState({
|
||||
events: Object.assign({}, this.state.events, {
|
||||
[eventId]: Object.assign({}, this.state.events[eventId], {
|
||||
registerState: 'Gas estimated, Posting transaction to the network'
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
const gasPassed = gas.mul(1.2);
|
||||
options.gas = gasPassed.toFixed(0);
|
||||
@ -335,19 +404,45 @@ export default class Application extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
registerUrl (url) {
|
||||
registerUrl (contentUrl) {
|
||||
const { contentHash, fromAddress, instance } = this.state;
|
||||
|
||||
this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' });
|
||||
|
||||
const values = [contentHash, url];
|
||||
const eventId = nextEventId++;
|
||||
const values = [contentHash, contentUrl];
|
||||
const options = { from: fromAddress };
|
||||
|
||||
this.setState({
|
||||
eventIds: [eventId].concat(this.state.eventIds),
|
||||
events: Object.assign({}, this.state.events, {
|
||||
[eventId]: {
|
||||
contentHash,
|
||||
contentUrl,
|
||||
fromAddress,
|
||||
registerBusy: true,
|
||||
registerState: 'Estimating gas for the transaction',
|
||||
timestamp: new Date()
|
||||
}
|
||||
}),
|
||||
url: '',
|
||||
commit: '',
|
||||
repo: '',
|
||||
commitError: null,
|
||||
contentHash: '',
|
||||
contentHashOwner: null,
|
||||
contentHashError: null
|
||||
});
|
||||
|
||||
this.trackRequest(
|
||||
instance
|
||||
eventId, instance
|
||||
.hintURL.estimateGas(options, values)
|
||||
.then((gas) => {
|
||||
this.setState({ registerState: 'Gas estimated, Posting transaction to the network' });
|
||||
this.setState({
|
||||
events: Object.assign({}, this.state.events, {
|
||||
[eventId]: Object.assign({}, this.state.events[eventId], {
|
||||
registerState: 'Gas estimated, Posting transaction to the network'
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
const gasPassed = gas.mul(1.2);
|
||||
options.gas = gasPassed.toFixed(0);
|
||||
|
37
js/src/dapps/githubhint/Events/events.css
Normal file
37
js/src/dapps/githubhint/Events/events.css
Normal file
@ -0,0 +1,37 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
.list {
|
||||
border: none;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
|
||||
tr {
|
||||
&[data-busy="true"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&[data-error="true"] {
|
||||
color: #f66;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
52
js/src/dapps/githubhint/Events/events.js
Normal file
52
js/src/dapps/githubhint/Events/events.js
Normal file
@ -0,0 +1,52 @@
|
||||
// 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 moment from 'moment';
|
||||
|
||||
import styles from './events.css';
|
||||
|
||||
export default class Events extends Component {
|
||||
static propTypes = {
|
||||
eventIds: PropTypes.array.isRequired,
|
||||
events: PropTypes.array.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<table className={ styles.list }>
|
||||
<tbody>
|
||||
{ this.props.eventIds.map((id) => this.renderEvent(id, this.props.events[id])) }
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
renderEvent = (eventId, event) => {
|
||||
return (
|
||||
<tr key={ `event_${eventId}` } data-busy={ event.registerBusy } data-error={ event.registerError }>
|
||||
<td>
|
||||
<div>{ moment(event.timestamp).fromNow() }</div>
|
||||
<div>{ event.registerState }</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{ event.contentUrl || `${event.contentRepo}/${event.contentCommit}` }</div>
|
||||
<div>{ event.contentHash }</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
17
js/src/dapps/githubhint/Events/index.js
Normal file
17
js/src/dapps/githubhint/Events/index.js
Normal 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 './events';
|
@ -14,9 +14,45 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { BlockNumber, Hash, Integer } from '../types';
|
||||
import { BlockNumber, Data, Hash, Integer } from '../types';
|
||||
|
||||
export default {
|
||||
block: {
|
||||
desc: 'Returns traces created at given block',
|
||||
params: [
|
||||
{
|
||||
type: BlockNumber,
|
||||
desc: 'Integer block number, or \'latest\' for the last mined block or \'pending\', \'earliest\' for not yet mined transactions'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: Array,
|
||||
desc: 'Block traces'
|
||||
}
|
||||
},
|
||||
|
||||
call: {
|
||||
desc: 'Returns traces for a specific call',
|
||||
params: [
|
||||
{
|
||||
type: Object,
|
||||
desc: 'Call options'
|
||||
},
|
||||
{
|
||||
type: BlockNumber,
|
||||
desc: 'The blockNumber'
|
||||
},
|
||||
{
|
||||
type: Array,
|
||||
desc: 'Type of trace, one or more of \'vmTrace\', \'trace\' and/or \'stateDiff\''
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: Array,
|
||||
desc: 'Block traces'
|
||||
}
|
||||
},
|
||||
|
||||
filter: {
|
||||
desc: 'Returns traces matching given filter',
|
||||
params: [
|
||||
@ -49,6 +85,42 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
rawTransaction: {
|
||||
desc: 'Traces a call to eth_sendRawTransaction without making the call, returning the traces',
|
||||
params: [
|
||||
{
|
||||
type: Data,
|
||||
desc: 'Transaction data'
|
||||
},
|
||||
{
|
||||
type: Array,
|
||||
desc: 'Type of trace, one or more of \'vmTrace\', \'trace\' and/or \'stateDiff\''
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: Array,
|
||||
desc: 'Block traces'
|
||||
}
|
||||
},
|
||||
|
||||
replayTransaction: {
|
||||
desc: 'Replays a transaction, returning the traces',
|
||||
params: [
|
||||
{
|
||||
type: Hash,
|
||||
desc: 'Transaction hash'
|
||||
},
|
||||
{
|
||||
type: Array,
|
||||
desc: 'Type of trace, one or more of \'vmTrace\', \'trace\' and/or \'stateDiff\''
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: Array,
|
||||
desc: 'Block traces'
|
||||
}
|
||||
},
|
||||
|
||||
transaction: {
|
||||
desc: 'Returns all traces of given transaction',
|
||||
params: [
|
||||
@ -61,19 +133,5 @@ export default {
|
||||
type: Array,
|
||||
desc: 'Traces of given transaction'
|
||||
}
|
||||
},
|
||||
|
||||
block: {
|
||||
desc: 'Returns traces created at given block',
|
||||
params: [
|
||||
{
|
||||
type: BlockNumber,
|
||||
desc: 'Integer block number, or \'latest\' for the last mined block or \'pending\', \'earliest\' for not yet mined transactions'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: Array,
|
||||
desc: 'Block traces'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -14,10 +14,22 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import 'babel-polyfill/dist/polyfill.js';
|
||||
import es6Promise from 'es6-promise';
|
||||
es6Promise.polyfill();
|
||||
|
||||
const isNode = typeof global !== 'undefined' && typeof global !== 'undefined';
|
||||
const isBrowser = typeof self !== 'undefined' && typeof self.window !== 'undefined';
|
||||
|
||||
if (isBrowser) {
|
||||
require('whatwg-fetch');
|
||||
}
|
||||
|
||||
if (isNode) {
|
||||
global.fetch = require('node-fetch');
|
||||
}
|
||||
|
||||
import Abi from './abi';
|
||||
import Api from './api';
|
||||
|
||||
export {
|
||||
Abi,
|
||||
Api
|
||||
};
|
||||
module.exports = { Api, Abi };
|
||||
|
@ -14,19 +14,3 @@
|
||||
/* You should have received a copy of the GNU General Public License
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.spaced {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.typeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.desc {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #ccc;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
@ -20,13 +20,10 @@ import ContentClear from 'material-ui/svg-icons/content/clear';
|
||||
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
|
||||
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
|
||||
|
||||
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||
|
||||
import { Button, Modal, Form, Input, InputAddress } from '../../ui';
|
||||
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '../../ui';
|
||||
import { ERRORS, validateAbi, validateAddress, validateName } from '../../util/validation';
|
||||
|
||||
import { eip20, wallet } from '../../contracts/abi';
|
||||
import styles from './addContract.css';
|
||||
|
||||
const ABI_TYPES = [
|
||||
{
|
||||
@ -105,13 +102,12 @@ export default class AddContract extends Component {
|
||||
const { abiTypeIndex } = this.state;
|
||||
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
valueSelected={ abiTypeIndex }
|
||||
<RadioButtons
|
||||
name='contractType'
|
||||
value={ abiTypeIndex }
|
||||
values={ this.getAbiTypes() }
|
||||
onChange={ this.onChangeABIType }
|
||||
>
|
||||
{ this.renderAbiTypes() }
|
||||
</RadioButtonGroup>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -194,20 +190,13 @@ export default class AddContract extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderAbiTypes () {
|
||||
return ABI_TYPES.map((type, index) => (
|
||||
<RadioButton
|
||||
className={ styles.spaced }
|
||||
value={ index }
|
||||
label={ (
|
||||
<div className={ styles.typeContainer }>
|
||||
<span>{ type.label }</span>
|
||||
<span className={ styles.desc }>{ type.description }</span>
|
||||
</div>
|
||||
) }
|
||||
key={ index }
|
||||
/>
|
||||
));
|
||||
getAbiTypes () {
|
||||
return ABI_TYPES.map((type, index) => ({
|
||||
label: type.label,
|
||||
description: type.description,
|
||||
key: index,
|
||||
...type
|
||||
}));
|
||||
}
|
||||
|
||||
onNext = () => {
|
||||
@ -218,8 +207,8 @@ export default class AddContract extends Component {
|
||||
this.setState({ step: this.state.step - 1 });
|
||||
}
|
||||
|
||||
onChangeABIType = (event, index) => {
|
||||
const abiType = ABI_TYPES[index];
|
||||
onChangeABIType = (value, index) => {
|
||||
const abiType = value || ABI_TYPES[index];
|
||||
this.setState({ abiTypeIndex: index, abiType });
|
||||
this.onEditAbi(abiType.value);
|
||||
}
|
||||
|
@ -15,13 +15,12 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { MenuItem } from 'material-ui';
|
||||
|
||||
import { AddressSelect, Form, Input, TypedInput } from '../../../ui';
|
||||
import { AddressSelect, Form, Input, Select } from '../../../ui';
|
||||
import { validateAbi } from '../../../util/validation';
|
||||
import { parseAbiType } from '../../../util/abi';
|
||||
|
||||
import styles from '../deployContract.css';
|
||||
|
||||
export default class DetailsStep extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
@ -29,24 +28,26 @@ export default class DetailsStep extends Component {
|
||||
|
||||
static propTypes = {
|
||||
accounts: PropTypes.object.isRequired,
|
||||
abi: PropTypes.string,
|
||||
abiError: PropTypes.string,
|
||||
code: PropTypes.string,
|
||||
codeError: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
descriptionError: PropTypes.string,
|
||||
|
||||
onFromAddressChange: PropTypes.func.isRequired,
|
||||
onNameChange: PropTypes.func.isRequired,
|
||||
onDescriptionChange: PropTypes.func.isRequired,
|
||||
onAbiChange: PropTypes.func.isRequired,
|
||||
onCodeChange: PropTypes.func.isRequired,
|
||||
onParamsChange: PropTypes.func.isRequired,
|
||||
onInputsChange: PropTypes.func.isRequired,
|
||||
|
||||
fromAddress: PropTypes.string,
|
||||
fromAddressError: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
nameError: PropTypes.string,
|
||||
params: PropTypes.array,
|
||||
paramsError: PropTypes.array,
|
||||
onAbiChange: PropTypes.func.isRequired,
|
||||
onCodeChange: PropTypes.func.isRequired,
|
||||
onFromAddressChange: PropTypes.func.isRequired,
|
||||
onDescriptionChange: PropTypes.func.isRequired,
|
||||
onNameChange: PropTypes.func.isRequired,
|
||||
onParamsChange: PropTypes.func.isRequired,
|
||||
description: PropTypes.string,
|
||||
descriptionError: PropTypes.string,
|
||||
abi: PropTypes.string,
|
||||
abiError: PropTypes.string,
|
||||
code: PropTypes.string,
|
||||
codeError: PropTypes.string,
|
||||
|
||||
readOnly: PropTypes.bool
|
||||
};
|
||||
|
||||
@ -55,7 +56,9 @@ export default class DetailsStep extends Component {
|
||||
};
|
||||
|
||||
state = {
|
||||
inputs: []
|
||||
solcOutput: '',
|
||||
contracts: {},
|
||||
selectedContractIndex: 0
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@ -63,6 +66,7 @@ export default class DetailsStep extends Component {
|
||||
|
||||
if (abi) {
|
||||
this.onAbiChange(abi);
|
||||
this.setState({ solcOutput: abi });
|
||||
}
|
||||
|
||||
if (code) {
|
||||
@ -71,8 +75,19 @@ export default class DetailsStep extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accounts } = this.props;
|
||||
const { abi, abiError, code, codeError, fromAddress, fromAddressError, name, nameError, readOnly } = this.props;
|
||||
const {
|
||||
accounts,
|
||||
readOnly,
|
||||
|
||||
fromAddress, fromAddressError,
|
||||
name, nameError,
|
||||
description, descriptionError,
|
||||
abiError,
|
||||
code, codeError
|
||||
} = this.props;
|
||||
|
||||
const { solcOutput, contracts } = this.state;
|
||||
const solc = contracts && Object.keys(contracts).length > 0;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
@ -83,18 +98,30 @@ export default class DetailsStep extends Component {
|
||||
error={ fromAddressError }
|
||||
accounts={ accounts }
|
||||
onChange={ this.onFromAddressChange } />
|
||||
|
||||
<Input
|
||||
label='contract name'
|
||||
hint='a name for the deployed contract'
|
||||
error={ nameError }
|
||||
value={ name }
|
||||
onSubmit={ this.onNameChange } />
|
||||
value={ name || '' }
|
||||
onChange={ this.onNameChange } />
|
||||
|
||||
<Input
|
||||
label='abi'
|
||||
hint='the abi of the contract to deploy'
|
||||
label='contract description (optional)'
|
||||
hint='a description for the contract'
|
||||
error={ descriptionError }
|
||||
value={ description }
|
||||
onChange={ this.onDescriptionChange } />
|
||||
|
||||
{ this.renderContractSelect() }
|
||||
|
||||
<Input
|
||||
label='abi / solc combined-output'
|
||||
hint='the abi of the contract to deploy or solc combined-output'
|
||||
error={ abiError }
|
||||
value={ abi }
|
||||
onSubmit={ this.onAbiChange }
|
||||
value={ solcOutput }
|
||||
onChange={ this.onSolcChange }
|
||||
onSubmit={ this.onSolcSubmit }
|
||||
readOnly={ readOnly } />
|
||||
<Input
|
||||
label='code'
|
||||
@ -102,66 +129,108 @@ export default class DetailsStep extends Component {
|
||||
error={ codeError }
|
||||
value={ code }
|
||||
onSubmit={ this.onCodeChange }
|
||||
readOnly={ readOnly } />
|
||||
readOnly={ readOnly || solc } />
|
||||
|
||||
{ this.renderConstructorInputs() }
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
renderConstructorInputs () {
|
||||
const { accounts, params, paramsError } = this.props;
|
||||
const { inputs } = this.state;
|
||||
renderContractSelect () {
|
||||
const { contracts } = this.state;
|
||||
|
||||
if (!inputs || !inputs.length) {
|
||||
if (!contracts || Object.keys(contracts).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return inputs.map((input, index) => {
|
||||
const onChange = (value) => this.onParamChange(index, value);
|
||||
const { selectedContractIndex } = this.state;
|
||||
const contractsItems = Object.keys(contracts).map((name, index) => (
|
||||
<MenuItem
|
||||
key={ index }
|
||||
label={ name }
|
||||
value={ index }
|
||||
>
|
||||
{ name }
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
const label = `${input.name ? `${input.name}: ` : ''}${input.type}`;
|
||||
const value = params[index];
|
||||
const error = paramsError[index];
|
||||
const param = parseAbiType(input.type);
|
||||
return (
|
||||
<Select
|
||||
label='select a contract'
|
||||
onChange={ this.onContractChange }
|
||||
value={ selectedContractIndex }
|
||||
>
|
||||
{ contractsItems }
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={ index } className={ styles.funcparams }>
|
||||
<TypedInput
|
||||
label={ label }
|
||||
value={ value }
|
||||
error={ error }
|
||||
accounts={ accounts }
|
||||
onChange={ onChange }
|
||||
param={ param }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
onContractChange = (event, index) => {
|
||||
const { contracts } = this.state;
|
||||
const contractName = Object.keys(contracts)[index];
|
||||
const contract = contracts[contractName];
|
||||
|
||||
if (!this.props.name || this.props.name.trim() === '') {
|
||||
this.onNameChange(null, contractName);
|
||||
}
|
||||
|
||||
const { abi, bin } = contract;
|
||||
const code = /^0x/.test(bin) ? bin : `0x${bin}`;
|
||||
|
||||
this.setState({ selectedContractIndex: index }, () => {
|
||||
this.onAbiChange(abi);
|
||||
this.onCodeChange(code);
|
||||
});
|
||||
}
|
||||
|
||||
onSolcChange = (event, value) => {
|
||||
// Change triggered only if valid
|
||||
if (this.props.abiError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.onSolcSubmit(value);
|
||||
}
|
||||
|
||||
onSolcSubmit = (value) => {
|
||||
try {
|
||||
const solcParsed = JSON.parse(value);
|
||||
|
||||
if (!solcParsed || !solcParsed.contracts) {
|
||||
throw new Error('Wrong solc output');
|
||||
}
|
||||
|
||||
this.setState({ contracts: solcParsed.contracts }, () => {
|
||||
this.onContractChange(null, 0);
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({ contracts: null });
|
||||
this.onAbiChange(value);
|
||||
}
|
||||
|
||||
this.setState({ solcOutput: value });
|
||||
}
|
||||
|
||||
onFromAddressChange = (event, fromAddress) => {
|
||||
const { onFromAddressChange } = this.props;
|
||||
|
||||
onFromAddressChange(fromAddress);
|
||||
}
|
||||
|
||||
onNameChange = (name) => {
|
||||
onNameChange = (event, name) => {
|
||||
const { onNameChange } = this.props;
|
||||
|
||||
onNameChange(name);
|
||||
}
|
||||
|
||||
onParamChange = (index, value) => {
|
||||
const { params, onParamsChange } = this.props;
|
||||
onDescriptionChange = (event, description) => {
|
||||
const { onDescriptionChange } = this.props;
|
||||
|
||||
params[index] = value;
|
||||
onParamsChange(params);
|
||||
onDescriptionChange(description);
|
||||
}
|
||||
|
||||
onAbiChange = (abi) => {
|
||||
const { api } = this.context;
|
||||
const { onAbiChange, onParamsChange } = this.props;
|
||||
const { onAbiChange, onParamsChange, onInputsChange } = this.props;
|
||||
const { abiError, abiParsed } = validateAbi(abi, api);
|
||||
|
||||
if (!abiError) {
|
||||
@ -176,10 +245,10 @@ export default class DetailsStep extends Component {
|
||||
});
|
||||
|
||||
onParamsChange(params);
|
||||
this.setState({ inputs });
|
||||
onInputsChange(inputs);
|
||||
} else {
|
||||
onParamsChange([]);
|
||||
this.setState({ inputs: [] });
|
||||
onInputsChange([]);
|
||||
}
|
||||
|
||||
onAbiChange(abi);
|
||||
|
17
js/src/modals/DeployContract/ParametersStep/index.js
Normal file
17
js/src/modals/DeployContract/ParametersStep/index.js
Normal 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 './parametersStep';
|
105
js/src/modals/DeployContract/ParametersStep/parametersStep.js
Normal file
105
js/src/modals/DeployContract/ParametersStep/parametersStep.js
Normal file
@ -0,0 +1,105 @@
|
||||
// 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/>.
|
||||
// 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 { Form, TypedInput } from '../../../ui';
|
||||
import { parseAbiType } from '../../../util/abi';
|
||||
|
||||
import styles from '../deployContract.css';
|
||||
|
||||
export default class ParametersStep extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
accounts: PropTypes.object.isRequired,
|
||||
onParamsChange: PropTypes.func.isRequired,
|
||||
|
||||
inputs: PropTypes.array,
|
||||
params: PropTypes.array,
|
||||
paramsError: PropTypes.array
|
||||
};
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Form>
|
||||
{ this.renderConstructorInputs() }
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
renderConstructorInputs () {
|
||||
const { accounts, params, paramsError } = this.props;
|
||||
const { inputs } = this.props;
|
||||
|
||||
if (!inputs || !inputs.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputsComponents = inputs.map((input, index) => {
|
||||
const onChange = (value) => this.onParamChange(index, value);
|
||||
|
||||
const label = `${input.name ? `${input.name}: ` : ''}${input.type}`;
|
||||
const value = params[index];
|
||||
const error = paramsError[index];
|
||||
const param = parseAbiType(input.type);
|
||||
|
||||
return (
|
||||
<div key={ index } className={ styles.funcparams }>
|
||||
<TypedInput
|
||||
label={ label }
|
||||
value={ value }
|
||||
error={ error }
|
||||
accounts={ accounts }
|
||||
onChange={ onChange }
|
||||
param={ param }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Choose the contract parameters</p>
|
||||
{ inputsComponents }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onParamChange = (index, value) => {
|
||||
const { params, onParamsChange } = this.props;
|
||||
|
||||
params[index] = value;
|
||||
onParamsChange(params);
|
||||
}
|
||||
}
|
@ -31,3 +31,7 @@
|
||||
.funcparams {
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
p {
|
||||
color: rgba(255, 255, 255, 0.498039);
|
||||
}
|
||||
|
@ -22,13 +22,19 @@ import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal,
|
||||
import { ERRORS, validateAbi, validateCode, validateName } from '../../util/validation';
|
||||
|
||||
import DetailsStep from './DetailsStep';
|
||||
import ParametersStep from './ParametersStep';
|
||||
import ErrorStep from './ErrorStep';
|
||||
|
||||
import styles from './deployContract.css';
|
||||
|
||||
import { ERROR_CODES } from '../../api/transport/error';
|
||||
|
||||
const steps = ['contract details', 'deployment', 'completed'];
|
||||
const STEPS = {
|
||||
CONTRACT_DETAILS: { title: 'contract details' },
|
||||
CONTRACT_PARAMETERS: { title: 'contract parameters' },
|
||||
DEPLOYMENT: { title: 'deployment', waiting: true },
|
||||
COMPLETED: { title: 'completed' }
|
||||
};
|
||||
|
||||
export default class DeployContract extends Component {
|
||||
static contextTypes = {
|
||||
@ -55,7 +61,6 @@ export default class DeployContract extends Component {
|
||||
abiError: ERRORS.invalidAbi,
|
||||
code: '',
|
||||
codeError: ERRORS.invalidCode,
|
||||
deployState: '',
|
||||
description: '',
|
||||
descriptionError: null,
|
||||
fromAddress: Object.keys(this.props.accounts)[0],
|
||||
@ -64,9 +69,12 @@ export default class DeployContract extends Component {
|
||||
nameError: ERRORS.invalidName,
|
||||
params: [],
|
||||
paramsError: [],
|
||||
step: 0,
|
||||
inputs: [],
|
||||
|
||||
deployState: '',
|
||||
deployError: null,
|
||||
rejected: false
|
||||
rejected: false,
|
||||
step: 'CONTRACT_DETAILS'
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
@ -95,20 +103,30 @@ export default class DeployContract extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { step, deployError, rejected } = this.state;
|
||||
const { step, deployError, rejected, inputs } = this.state;
|
||||
|
||||
const realStep = Object.keys(STEPS).findIndex((k) => k === step);
|
||||
const realSteps = deployError || rejected
|
||||
? null
|
||||
: Object.keys(STEPS)
|
||||
.filter((k) => k !== 'CONTRACT_PARAMETERS' || inputs.length > 0)
|
||||
.map((k) => STEPS[k]);
|
||||
|
||||
const realSteps = deployError || rejected ? null : steps;
|
||||
const title = realSteps
|
||||
? null
|
||||
: (deployError ? 'deployment failed' : 'rejected');
|
||||
|
||||
const waiting = realSteps
|
||||
? realSteps.map((s, i) => s.waiting ? i : false).filter((v) => v !== false)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
actions={ this.renderDialogActions() }
|
||||
current={ step }
|
||||
steps={ realSteps }
|
||||
current={ realStep }
|
||||
steps={ realSteps ? realSteps.map((s) => s.title) : null }
|
||||
title={ title }
|
||||
waiting={ realSteps ? [1] : null }
|
||||
waiting={ waiting }
|
||||
visible
|
||||
scroll>
|
||||
{ this.renderStep() }
|
||||
@ -146,20 +164,29 @@ export default class DeployContract extends Component {
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 0:
|
||||
case 'CONTRACT_DETAILS':
|
||||
return [
|
||||
cancelBtn,
|
||||
<Button
|
||||
disabled={ !isValid }
|
||||
icon={ <IdentityIcon button address={ fromAddress } /> }
|
||||
label='Next'
|
||||
onClick={ this.onParametersStep } />
|
||||
];
|
||||
|
||||
case 'CONTRACT_PARAMETERS':
|
||||
return [
|
||||
cancelBtn,
|
||||
<Button
|
||||
icon={ <IdentityIcon button address={ fromAddress } /> }
|
||||
label='Create'
|
||||
onClick={ this.onDeployStart } />
|
||||
];
|
||||
|
||||
case 1:
|
||||
case 'DEPLOYMENT':
|
||||
return [ closeBtn ];
|
||||
|
||||
case 2:
|
||||
case 'COMPLETED':
|
||||
return [ closeBtnOk ];
|
||||
}
|
||||
}
|
||||
@ -184,21 +211,33 @@ export default class DeployContract extends Component {
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 0:
|
||||
case 'CONTRACT_DETAILS':
|
||||
return (
|
||||
<DetailsStep
|
||||
{ ...this.state }
|
||||
readOnly={ readOnly }
|
||||
accounts={ accounts }
|
||||
onAbiChange={ this.onAbiChange }
|
||||
onCodeChange={ this.onCodeChange }
|
||||
readOnly={ readOnly }
|
||||
onFromAddressChange={ this.onFromAddressChange }
|
||||
onDescriptionChange={ this.onDescriptionChange }
|
||||
onNameChange={ this.onNameChange }
|
||||
onParamsChange={ this.onParamsChange } />
|
||||
onAbiChange={ this.onAbiChange }
|
||||
onCodeChange={ this.onCodeChange }
|
||||
onParamsChange={ this.onParamsChange }
|
||||
onInputsChange={ this.onInputsChange }
|
||||
/>
|
||||
);
|
||||
|
||||
case 1:
|
||||
case 'CONTRACT_PARAMETERS':
|
||||
return (
|
||||
<ParametersStep
|
||||
{ ...this.state }
|
||||
readOnly={ readOnly }
|
||||
accounts={ accounts }
|
||||
onParamsChange={ this.onParamsChange }
|
||||
/>
|
||||
);
|
||||
|
||||
case 'DEPLOYMENT':
|
||||
const body = txhash
|
||||
? <TxHash hash={ txhash } />
|
||||
: null;
|
||||
@ -210,7 +249,7 @@ export default class DeployContract extends Component {
|
||||
</BusyStep>
|
||||
);
|
||||
|
||||
case 2:
|
||||
case 'COMPLETED':
|
||||
return (
|
||||
<CompletedStep>
|
||||
<div>Your contract has been deployed at</div>
|
||||
@ -225,12 +264,23 @@ export default class DeployContract extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onParametersStep = () => {
|
||||
const { inputs } = this.state;
|
||||
|
||||
if (inputs.length) {
|
||||
return this.setState({ step: 'CONTRACT_PARAMETERS' });
|
||||
}
|
||||
|
||||
return this.onDeployStart();
|
||||
}
|
||||
|
||||
onDescriptionChange = (description) => {
|
||||
this.setState({ description, descriptionError: null });
|
||||
}
|
||||
|
||||
onFromAddressChange = (fromAddress) => {
|
||||
const { api } = this.context;
|
||||
|
||||
const fromAddressError = api.util.isAddressValid(fromAddress)
|
||||
? null
|
||||
: 'a valid account as the contract owner needs to be selected';
|
||||
@ -246,6 +296,10 @@ export default class DeployContract extends Component {
|
||||
this.setState({ params });
|
||||
}
|
||||
|
||||
onInputsChange = (inputs) => {
|
||||
this.setState({ inputs });
|
||||
}
|
||||
|
||||
onAbiChange = (abi) => {
|
||||
const { api } = this.context;
|
||||
|
||||
@ -267,7 +321,7 @@ export default class DeployContract extends Component {
|
||||
from: fromAddress
|
||||
};
|
||||
|
||||
this.setState({ step: 1 });
|
||||
this.setState({ step: 'DEPLOYMENT' });
|
||||
|
||||
api
|
||||
.newContract(abiParsed)
|
||||
@ -286,7 +340,7 @@ export default class DeployContract extends Component {
|
||||
])
|
||||
.then(() => {
|
||||
console.log(`contract deployed at ${address}`);
|
||||
this.setState({ step: 2, address });
|
||||
this.setState({ step: 'DEPLOYMENT', address });
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
|
31
js/src/modals/SMSVerification/Done/done.css
Normal file
31
js/src/modals/SMSVerification/Done/done.css
Normal file
@ -0,0 +1,31 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
.spacing {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: .5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: .5em;
|
||||
}
|
31
js/src/modals/SMSVerification/Done/done.js
Normal file
31
js/src/modals/SMSVerification/Done/done.js
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 } from 'react';
|
||||
import SuccessIcon from 'material-ui/svg-icons/navigation/check';
|
||||
|
||||
import styles from './done.css';
|
||||
|
||||
export default class Done extends Component {
|
||||
render () {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<SuccessIcon />
|
||||
<p className={ styles.message }>Congratulations, your account is verified!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
17
js/src/modals/SMSVerification/Done/index.js
Normal file
17
js/src/modals/SMSVerification/Done/index.js
Normal 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 './done';
|
49
js/src/modals/SMSVerification/GatherData/gatherData.css
Normal file
49
js/src/modals/SMSVerification/GatherData/gatherData.css
Normal file
@ -0,0 +1,49 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
.list li {
|
||||
padding: .1em 0;
|
||||
}
|
||||
|
||||
.spacing {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: .5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.message {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.terms {
|
||||
line-height: 1.3;
|
||||
opacity: .7;
|
||||
|
||||
ul {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-top: .2em;
|
||||
margin-bottom: .2em;
|
||||
}
|
||||
}
|
151
js/src/modals/SMSVerification/GatherData/gatherData.js
Normal file
151
js/src/modals/SMSVerification/GatherData/gatherData.js
Normal file
@ -0,0 +1,151 @@
|
||||
// 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 nullable from '../../../util/nullable-proptype';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Checkbox } from 'material-ui';
|
||||
import InfoIcon from 'material-ui/svg-icons/action/info-outline';
|
||||
import SuccessIcon from 'material-ui/svg-icons/navigation/check';
|
||||
import ErrorIcon from 'material-ui/svg-icons/navigation/close';
|
||||
|
||||
import { fromWei } from '../../../api/util/wei';
|
||||
import { Form, Input } from '../../../ui';
|
||||
|
||||
import terms from '../terms-of-service';
|
||||
import styles from './gatherData.css';
|
||||
|
||||
export default class GatherData extends Component {
|
||||
static propTypes = {
|
||||
fee: React.PropTypes.instanceOf(BigNumber),
|
||||
isNumberValid: PropTypes.bool.isRequired,
|
||||
isVerified: nullable(PropTypes.bool.isRequired),
|
||||
hasRequested: nullable(PropTypes.bool.isRequired),
|
||||
setNumber: PropTypes.func.isRequired,
|
||||
setConsentGiven: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isNumberValid, isVerified } = this.props;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<p>The following steps will let you prove that you control both an account and a phone number.</p>
|
||||
<ol className={ styles.list }>
|
||||
<li>You send a verification request to a specific contract.</li>
|
||||
<li>Our server puts a puzzle into this contract.</li>
|
||||
<li>The code you receive via SMS is the solution to this puzzle.</li>
|
||||
</ol>
|
||||
{ this.renderFee() }
|
||||
{ this.renderCertified() }
|
||||
{ this.renderRequested() }
|
||||
<Input
|
||||
label={ 'phone number' }
|
||||
hint={ 'the SMS will be sent to this number' }
|
||||
error={ isNumberValid ? null : 'invalid number' }
|
||||
disabled={ isVerified }
|
||||
onChange={ this.numberOnChange }
|
||||
onSubmit={ this.numberOnSubmit }
|
||||
/>
|
||||
<Checkbox
|
||||
className={ styles.spacing }
|
||||
label={ 'I agree to the terms and conditions below.' }
|
||||
disabled={ isVerified }
|
||||
onCheck={ this.consentOnChange }
|
||||
/>
|
||||
<div className={ styles.terms }>{ terms }</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
renderFee () {
|
||||
const { fee } = this.props;
|
||||
|
||||
if (!fee) {
|
||||
return (<p>Fetching the fee…</p>);
|
||||
}
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<InfoIcon />
|
||||
<p className={ styles.message }>The fee is { fromWei(fee).toFixed(3) } ETH.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCertified () {
|
||||
const { isVerified } = this.props;
|
||||
|
||||
if (isVerified) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<ErrorIcon />
|
||||
<p className={ styles.message }>Your account is already verified.</p>
|
||||
</div>
|
||||
);
|
||||
} else if (isVerified === false) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<SuccessIcon />
|
||||
<p className={ styles.message }>Your account is not verified yet.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p className={ styles.message }>Checking if your account is verified…</p>
|
||||
);
|
||||
}
|
||||
|
||||
renderRequested () {
|
||||
const { isVerified, hasRequested } = this.props;
|
||||
|
||||
// If the account is verified, don't show that it has requested verification.
|
||||
if (isVerified) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasRequested) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<InfoIcon />
|
||||
<p className={ styles.message }>You already requested verification.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (hasRequested === false) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<SuccessIcon />
|
||||
<p className={ styles.message }>You did not request verification yet.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p className={ styles.message }>Checking if you requested verification…</p>
|
||||
);
|
||||
}
|
||||
|
||||
numberOnSubmit = (value) => {
|
||||
this.props.setNumber(value);
|
||||
}
|
||||
|
||||
numberOnChange = (_, value) => {
|
||||
this.props.setNumber(value);
|
||||
}
|
||||
|
||||
consentOnChange = (_, consentGiven) => {
|
||||
this.props.setConsentGiven(consentGiven);
|
||||
}
|
||||
}
|
17
js/src/modals/SMSVerification/GatherData/index.js
Normal file
17
js/src/modals/SMSVerification/GatherData/index.js
Normal 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 './gatherData';
|
17
js/src/modals/SMSVerification/QueryCode/index.js
Normal file
17
js/src/modals/SMSVerification/QueryCode/index.js
Normal 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 './queryCode';
|
52
js/src/modals/SMSVerification/QueryCode/queryCode.js
Normal file
52
js/src/modals/SMSVerification/QueryCode/queryCode.js
Normal file
@ -0,0 +1,52 @@
|
||||
// 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 { Form, Input } from '../../../ui';
|
||||
|
||||
export default class QueryCode extends Component {
|
||||
static propTypes = {
|
||||
number: PropTypes.string.isRequired,
|
||||
isCodeValid: PropTypes.bool.isRequired,
|
||||
setCode: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { number, isCodeValid } = this.props;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<p>The verification code has been sent to { number }.</p>
|
||||
<Input
|
||||
label={ 'verification code' }
|
||||
hint={ 'Enter the code you received via SMS.' }
|
||||
error={ isCodeValid ? null : 'invalid code' }
|
||||
onChange={ this.onChange }
|
||||
onSubmit={ this.onSubmit }
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
onChange = (_, code) => {
|
||||
this.props.setCode(code.trim());
|
||||
}
|
||||
|
||||
onSubmit = (code) => {
|
||||
this.props.setCode(code.trim());
|
||||
}
|
||||
}
|
176
js/src/modals/SMSVerification/SMSVerification.js
Normal file
176
js/src/modals/SMSVerification/SMSVerification.js
Normal file
@ -0,0 +1,176 @@
|
||||
// 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 { observer } from 'mobx-react';
|
||||
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
||||
|
||||
import { Button, IdentityIcon, Modal } from '../../ui';
|
||||
|
||||
import {
|
||||
LOADING,
|
||||
QUERY_DATA,
|
||||
POSTING_REQUEST, POSTED_REQUEST,
|
||||
REQUESTING_SMS, REQUESTED_SMS,
|
||||
POSTING_CONFIRMATION, POSTED_CONFIRMATION,
|
||||
DONE
|
||||
} from './store';
|
||||
|
||||
import GatherData from './GatherData';
|
||||
import SendRequest from './SendRequest';
|
||||
import QueryCode from './QueryCode';
|
||||
import SendConfirmation from './SendConfirmation';
|
||||
import Done from './Done';
|
||||
|
||||
@observer
|
||||
export default class SMSVerification extends Component {
|
||||
static propTypes = {
|
||||
store: PropTypes.any.isRequired,
|
||||
account: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
static phases = { // mapping (store steps -> steps)
|
||||
[LOADING]: 0,
|
||||
[QUERY_DATA]: 1,
|
||||
[POSTING_REQUEST]: 2, [POSTED_REQUEST]: 2, [REQUESTING_SMS]: 2,
|
||||
[REQUESTED_SMS]: 3,
|
||||
[POSTING_CONFIRMATION]: 4, [POSTED_CONFIRMATION]: 4,
|
||||
[DONE]: 5
|
||||
}
|
||||
|
||||
render () {
|
||||
const phase = SMSVerification.phases[this.props.store.step];
|
||||
const { error, isStepValid } = this.props.store;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
actions={ this.renderDialogActions(phase, error, isStepValid) }
|
||||
title='verify your account via SMS'
|
||||
visible scroll
|
||||
current={ phase }
|
||||
steps={ ['Prepare', 'Enter Data', 'Request', 'Enter Code', 'Confirm', 'Done!'] }
|
||||
waiting={ error ? [] : [ 0, 2, 4 ] }
|
||||
>
|
||||
{ this.renderStep(phase, error) }
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderDialogActions (phase, error, isStepValid) {
|
||||
const { store, account, onClose } = this.props;
|
||||
|
||||
const cancel = (
|
||||
<Button
|
||||
key='cancel' label='Cancel'
|
||||
icon={ <ContentClear /> }
|
||||
onClick={ onClose }
|
||||
/>
|
||||
);
|
||||
if (error) {
|
||||
return (<div>{ cancel }</div>);
|
||||
}
|
||||
|
||||
if (phase === 5) {
|
||||
return (
|
||||
<div>
|
||||
{ cancel }
|
||||
<Button
|
||||
key='done' label='Done'
|
||||
disabled={ !isStepValid }
|
||||
icon={ <ActionDoneAll /> }
|
||||
onClick={ onClose }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let action = () => {};
|
||||
switch (phase) {
|
||||
case 1:
|
||||
action = store.sendRequest;
|
||||
break;
|
||||
case 2:
|
||||
action = store.queryCode;
|
||||
break;
|
||||
case 3:
|
||||
action = store.sendConfirmation;
|
||||
break;
|
||||
case 4:
|
||||
action = store.done;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ cancel }
|
||||
<Button
|
||||
key='next' label='Next'
|
||||
disabled={ !isStepValid }
|
||||
icon={ <IdentityIcon address={ account } button /> }
|
||||
onClick={ action }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStep (phase, error) {
|
||||
if (error) {
|
||||
return (<p>{ error }</p>);
|
||||
}
|
||||
|
||||
const {
|
||||
step,
|
||||
fee, number, isNumberValid, isVerified, hasRequested,
|
||||
requestTx, isCodeValid, confirmationTx,
|
||||
setNumber, setConsentGiven, setCode
|
||||
} = this.props.store;
|
||||
|
||||
if (phase === 5) {
|
||||
return (<Done />);
|
||||
}
|
||||
if (phase === 4) {
|
||||
return (<SendConfirmation step={ step } tx={ confirmationTx } />);
|
||||
}
|
||||
if (phase === 3) {
|
||||
return (
|
||||
<QueryCode
|
||||
number={ number } fee={ fee } isCodeValid={ isCodeValid }
|
||||
setCode={ setCode }
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (phase === 2) {
|
||||
return (<SendRequest step={ step } tx={ requestTx } />);
|
||||
}
|
||||
if (phase === 1) {
|
||||
const { setNumber, setConsentGiven } = this.props.store;
|
||||
return (
|
||||
<GatherData
|
||||
fee={ fee } isNumberValid={ isNumberValid }
|
||||
isVerified={ isVerified } hasRequested={ hasRequested }
|
||||
setNumber={ setNumber } setConsentGiven={ setConsentGiven }
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (phase === 0) {
|
||||
return (<p>Preparing awesomeness!</p>);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
17
js/src/modals/SMSVerification/SendConfirmation/index.js
Normal file
17
js/src/modals/SMSVerification/SendConfirmation/index.js
Normal 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 './sendConfirmation';
|
@ -0,0 +1,20 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
// 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 nullable from '../../../util/nullable-proptype';
|
||||
|
||||
import TxHash from '../../../ui/TxHash';
|
||||
import {
|
||||
POSTING_CONFIRMATION, POSTED_CONFIRMATION
|
||||
} from '../store';
|
||||
|
||||
import styles from './sendConfirmation.css';
|
||||
|
||||
export default class SendConfirmation extends Component {
|
||||
static propTypes = {
|
||||
step: PropTypes.any.isRequired,
|
||||
tx: nullable(PropTypes.any.isRequired)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { step, tx } = this.props;
|
||||
|
||||
if (step === POSTING_CONFIRMATION) {
|
||||
return (<p>The verification code will be sent to the contract. Please authorize this using the Parity Signer.</p>);
|
||||
}
|
||||
|
||||
if (step === POSTED_CONFIRMATION) {
|
||||
return (
|
||||
<div className={ styles.centered }>
|
||||
<TxHash hash={ tx } maxConfirmations={ 2 } />
|
||||
<p>Please keep this window open.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
17
js/src/modals/SMSVerification/SendRequest/index.js
Normal file
17
js/src/modals/SMSVerification/SendRequest/index.js
Normal 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 './sendRequest';
|
20
js/src/modals/SMSVerification/SendRequest/sendRequest.css
Normal file
20
js/src/modals/SMSVerification/SendRequest/sendRequest.css
Normal file
@ -0,0 +1,20 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
57
js/src/modals/SMSVerification/SendRequest/sendRequest.js
Normal file
57
js/src/modals/SMSVerification/SendRequest/sendRequest.js
Normal file
@ -0,0 +1,57 @@
|
||||
// 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 nullable from '../../../util/nullable-proptype';
|
||||
|
||||
import TxHash from '../../../ui/TxHash';
|
||||
import {
|
||||
POSTING_REQUEST, POSTED_REQUEST, REQUESTING_SMS
|
||||
} from '../store';
|
||||
|
||||
import styles from './sendRequest.css';
|
||||
|
||||
export default class SendRequest extends Component {
|
||||
static propTypes = {
|
||||
step: PropTypes.any.isRequired,
|
||||
tx: nullable(PropTypes.any.isRequired)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { step, tx } = this.props;
|
||||
|
||||
switch (step) {
|
||||
case POSTING_REQUEST:
|
||||
return (<p>A verification request will be sent to the contract. Please authorize this using the Parity Signer.</p>);
|
||||
|
||||
case POSTED_REQUEST:
|
||||
return (
|
||||
<div className={ styles.centered }>
|
||||
<TxHash hash={ tx } maxConfirmations={ 1 } />
|
||||
<p>Please keep this window open.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
case REQUESTING_SMS:
|
||||
return (
|
||||
<p>Requesting an SMS from the Parity server.</p>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
17
js/src/modals/SMSVerification/index.js
Normal file
17
js/src/modals/SMSVerification/index.js
Normal 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 './SMSVerification';
|
246
js/src/modals/SMSVerification/store.js
Normal file
246
js/src/modals/SMSVerification/store.js
Normal file
@ -0,0 +1,246 @@
|
||||
// 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 { observable, computed, autorun, action } from 'mobx';
|
||||
import phone from 'phoneformat.js';
|
||||
import { sha3 } from '../../api/util/sha3';
|
||||
|
||||
import Contracts from '../../contracts';
|
||||
|
||||
import { checkIfVerified, checkIfRequested, postToServer } from '../../contracts/sms-verification';
|
||||
import checkIfTxFailed from '../../util/check-if-tx-failed';
|
||||
import waitForConfirmations from '../../util/wait-for-block-confirmations';
|
||||
|
||||
const validCode = /^[A-Z\s]+$/i;
|
||||
|
||||
export const LOADING = 'fetching-contract';
|
||||
export const QUERY_DATA = 'query-data';
|
||||
export const POSTING_REQUEST = 'posting-request';
|
||||
export const POSTED_REQUEST = 'posted-request';
|
||||
export const REQUESTING_SMS = 'requesting-sms';
|
||||
export const REQUESTED_SMS = 'requested-sms';
|
||||
export const QUERY_CODE = 'query-code';
|
||||
export const POSTING_CONFIRMATION = 'posting-confirmation';
|
||||
export const POSTED_CONFIRMATION = 'posted-confirmation';
|
||||
export const DONE = 'done';
|
||||
|
||||
export default class VerificationStore {
|
||||
@observable step = null;
|
||||
@observable error = null;
|
||||
|
||||
@observable contract = null;
|
||||
@observable fee = null;
|
||||
@observable isVerified = null;
|
||||
@observable hasRequested = null;
|
||||
@observable consentGiven = false;
|
||||
@observable number = '';
|
||||
@observable requestTx = null;
|
||||
@observable code = '';
|
||||
@observable confirmationTx = null;
|
||||
|
||||
@computed get isCodeValid () {
|
||||
return validCode.test(this.code);
|
||||
}
|
||||
@computed get isNumberValid () {
|
||||
return phone.isValidNumber(this.number);
|
||||
}
|
||||
|
||||
@computed get isStepValid () {
|
||||
if (this.step === DONE) {
|
||||
return true;
|
||||
}
|
||||
if (this.error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.step) {
|
||||
case LOADING:
|
||||
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
|
||||
case QUERY_DATA:
|
||||
return this.isNumberValid && this.consentGiven;
|
||||
case REQUESTED_SMS:
|
||||
return this.requestTx;
|
||||
case QUERY_CODE:
|
||||
return this.isCodeValid;
|
||||
case POSTED_CONFIRMATION:
|
||||
return this.confirmationTx;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
constructor (api, account) {
|
||||
this.api = api;
|
||||
this.account = account;
|
||||
|
||||
this.step = LOADING;
|
||||
Contracts.create(api).registry.getContract('smsVerification')
|
||||
.then((contract) => {
|
||||
this.contract = contract;
|
||||
this.load();
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to fetch the contract: ' + err.message;
|
||||
});
|
||||
|
||||
autorun(() => {
|
||||
if (this.error) {
|
||||
console.error('sms verification: ' + this.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@action load = () => {
|
||||
const { contract, account } = this;
|
||||
this.step = LOADING;
|
||||
|
||||
const fee = contract.instance.fee.call()
|
||||
.then((fee) => {
|
||||
this.fee = fee;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to fetch the fee: ' + err.message;
|
||||
});
|
||||
|
||||
const isVerified = checkIfVerified(contract, account)
|
||||
.then((isVerified) => {
|
||||
this.isVerified = isVerified;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to check if verified: ' + err.message;
|
||||
});
|
||||
|
||||
const hasRequested = checkIfRequested(contract, account)
|
||||
.then((txHash) => {
|
||||
this.hasRequested = !!txHash;
|
||||
if (txHash) {
|
||||
this.requestTx = txHash;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to check if requested: ' + err.message;
|
||||
});
|
||||
|
||||
Promise
|
||||
.all([ fee, isVerified, hasRequested ])
|
||||
.then(() => {
|
||||
this.step = QUERY_DATA;
|
||||
});
|
||||
}
|
||||
|
||||
@action setNumber = (number) => {
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
@action setConsentGiven = (consentGiven) => {
|
||||
this.consentGiven = consentGiven;
|
||||
}
|
||||
|
||||
@action setCode = (code) => {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
@action sendRequest = () => {
|
||||
const { api, account, contract, fee, number, hasRequested } = this;
|
||||
|
||||
const request = contract.functions.find((fn) => fn.name === 'request');
|
||||
const options = { from: account, value: fee.toString() };
|
||||
|
||||
let chain = Promise.resolve();
|
||||
if (!hasRequested) {
|
||||
this.step = POSTING_REQUEST;
|
||||
chain = request.estimateGas(options, [])
|
||||
.then((gas) => {
|
||||
options.gas = gas.mul(1.2).toFixed(0);
|
||||
return request.postTransaction(options, []);
|
||||
})
|
||||
.then((handle) => {
|
||||
// TODO: The "request rejected" error doesn't have any property to
|
||||
// distinguish it from other errors, so we can't give a meaningful error here.
|
||||
return api.pollMethod('parity_checkRequest', handle);
|
||||
})
|
||||
.then((txHash) => {
|
||||
this.requestTx = txHash;
|
||||
return checkIfTxFailed(api, txHash, options.gas)
|
||||
.then((hasFailed) => {
|
||||
if (hasFailed) {
|
||||
throw new Error('Transaction failed, all gas used up.');
|
||||
}
|
||||
this.step = POSTED_REQUEST;
|
||||
return waitForConfirmations(api, txHash, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
chain
|
||||
.then(() => {
|
||||
this.step = REQUESTING_SMS;
|
||||
return postToServer({ number, address: account });
|
||||
})
|
||||
.then(() => {
|
||||
this.step = REQUESTED_SMS;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to request a confirmation SMS: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
@action queryCode = () => {
|
||||
this.step = QUERY_CODE;
|
||||
}
|
||||
|
||||
@action sendConfirmation = () => {
|
||||
const { api, account, contract, code } = this;
|
||||
const token = sha3(code);
|
||||
|
||||
const confirm = contract.functions.find((fn) => fn.name === 'confirm');
|
||||
const options = { from: account };
|
||||
const values = [ token ];
|
||||
|
||||
this.step = POSTING_CONFIRMATION;
|
||||
confirm.estimateGas(options, values)
|
||||
.then((gas) => {
|
||||
options.gas = gas.mul(1.2).toFixed(0);
|
||||
return confirm.postTransaction(options, values);
|
||||
})
|
||||
.then((handle) => {
|
||||
// TODO: The "request rejected" error doesn't have any property to
|
||||
// distinguish it from other errors, so we can't give a meaningful error here.
|
||||
return api.pollMethod('parity_checkRequest', handle);
|
||||
})
|
||||
.then((txHash) => {
|
||||
this.confirmationTx = txHash;
|
||||
return checkIfTxFailed(api, txHash, options.gas)
|
||||
.then((hasFailed) => {
|
||||
if (hasFailed) {
|
||||
throw new Error('Transaction failed, all gas used up.');
|
||||
}
|
||||
this.step = POSTED_CONFIRMATION;
|
||||
return waitForConfirmations(api, txHash, 1);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
this.step = DONE;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to send the verification code: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
@action done = () => {
|
||||
this.step = DONE;
|
||||
}
|
||||
}
|
27
js/src/modals/SMSVerification/terms-of-service.js
Normal file
27
js/src/modals/SMSVerification/terms-of-service.js
Normal file
@ -0,0 +1,27 @@
|
||||
// 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 from 'react';
|
||||
|
||||
export default (
|
||||
<ul>
|
||||
<li>This privacy notice relates to your use of the Parity SMS verification service. We take your privacy seriously and deal in an honest, direct and transparent way when it comes to your data.</li>
|
||||
<li>We collect your phone number when you use this service. This is temporarily kept in memory, and then encrypted and stored in our EU servers. We only retain the cryptographic hash of the number to prevent duplicated accounts. You consent to this use.</li>
|
||||
<li>You pay a fee for the cost of this service using the account you want to verify.</li>
|
||||
<li>Your phone number is transmitted to a third party US SMS verification service Twilio for the sole purpose of the SMS verification. You consent to this use. Twilio’s privacy policy is here: <a href={ 'https://www.twilio.com/legal/privacy/developer' }>https://www.twilio.com/legal/privacy/developer</a>.</li>
|
||||
<li><i>Parity Technology Limited</i> is registered in England and Wales under company number <code>09760015</code> and complies with the Data Protection Act 1998 (UK). You may contact us via email at <a href={ 'mailto:admin@parity.io' }>admin@parity.io</a>. Our general privacy policy can be found here: <a href={ 'https://ethcore.io/legal.html' }>https://ethcore.io/legal.html</a>.</li>
|
||||
</ul>
|
||||
);
|
@ -22,6 +22,7 @@ import EditMeta from './EditMeta';
|
||||
import ExecuteContract from './ExecuteContract';
|
||||
import FirstRun from './FirstRun';
|
||||
import Shapeshift from './Shapeshift';
|
||||
import SMSVerification from './SMSVerification';
|
||||
import Transfer from './Transfer';
|
||||
import PasswordManager from './PasswordManager';
|
||||
import SaveContract from './SaveContract';
|
||||
@ -36,6 +37,7 @@ export {
|
||||
ExecuteContract,
|
||||
FirstRun,
|
||||
Shapeshift,
|
||||
SMSVerification,
|
||||
Transfer,
|
||||
PasswordManager,
|
||||
LoadContract,
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { newError } from '../ui/Errors/actions';
|
||||
import { setAddressImage } from './providers/imagesActions';
|
||||
import { clearStatusLogs, toggleStatusLogs } from './providers/statusActions';
|
||||
import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions';
|
||||
import { toggleView } from '../views/Settings';
|
||||
|
||||
export {
|
||||
@ -24,5 +24,6 @@ export {
|
||||
clearStatusLogs,
|
||||
setAddressImage,
|
||||
toggleStatusLogs,
|
||||
toggleStatusRefresh,
|
||||
toggleView
|
||||
};
|
||||
|
@ -15,32 +15,29 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { statusBlockNumber, statusCollection, statusLogs } from './statusActions';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export default class Status {
|
||||
constructor (store, api) {
|
||||
this._api = api;
|
||||
this._store = store;
|
||||
|
||||
this._pingable = false;
|
||||
this._apiStatus = {};
|
||||
this._status = {};
|
||||
this._longStatus = {};
|
||||
this._minerSettings = {};
|
||||
|
||||
this._pollPingTimeoutId = null;
|
||||
this._longStatusTimeoutId = null;
|
||||
}
|
||||
|
||||
start () {
|
||||
this._subscribeBlockNumber();
|
||||
this._pollPing();
|
||||
this._pollStatus();
|
||||
this._pollLongStatus();
|
||||
this._pollLogs();
|
||||
this._fetchEnode();
|
||||
}
|
||||
|
||||
_fetchEnode () {
|
||||
this._api.parity
|
||||
.enode()
|
||||
.then((enode) => {
|
||||
this._store.dispatch(statusCollection({ enode }));
|
||||
})
|
||||
.catch(() => {
|
||||
window.setTimeout(() => {
|
||||
this._fetchEnode();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
_subscribeBlockNumber () {
|
||||
@ -51,16 +48,58 @@ export default class Status {
|
||||
}
|
||||
|
||||
this._store.dispatch(statusBlockNumber(blockNumber));
|
||||
|
||||
this._api.eth
|
||||
.getBlockByNumber(blockNumber)
|
||||
.then((block) => {
|
||||
this._store.dispatch(statusCollection({ gasLimit: block.gasLimit }));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
|
||||
});
|
||||
})
|
||||
.then((subscriptionId) => {
|
||||
console.log('status._subscribeBlockNumber', 'subscriptionId', subscriptionId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pinging should be smart. It should only
|
||||
* be used when the UI is connecting or the
|
||||
* Node is deconnected.
|
||||
*
|
||||
* @see src/views/Connection/connection.js
|
||||
*/
|
||||
_shouldPing = () => {
|
||||
const { isConnected, isConnecting } = this._apiStatus;
|
||||
return isConnecting || !isConnected;
|
||||
}
|
||||
|
||||
_stopPollPing = () => {
|
||||
if (!this._pollPingTimeoutId) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this._pollPingTimeoutId);
|
||||
this._pollPingTimeoutId = null;
|
||||
}
|
||||
|
||||
_pollPing = () => {
|
||||
const dispatch = (status, timeout = 500) => {
|
||||
this._store.dispatch(statusCollection({ isPingable: status }));
|
||||
setTimeout(this._pollPing, timeout);
|
||||
// Already pinging, don't try again
|
||||
if (this._pollPingTimeoutId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dispatch = (pingable, timeout = 1000) => {
|
||||
if (pingable !== this._pingable) {
|
||||
this._pingable = pingable;
|
||||
this._store.dispatch(statusCollection({ isPingable: pingable }));
|
||||
}
|
||||
|
||||
this._pollPingTimeoutId = setTimeout(() => {
|
||||
this._stopPollPing();
|
||||
this._pollPing();
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
fetch('/', { method: 'HEAD' })
|
||||
@ -79,61 +118,162 @@ export default class Status {
|
||||
}
|
||||
|
||||
_pollStatus = () => {
|
||||
const { secureToken, isConnected, isConnecting, needsToken } = this._api;
|
||||
|
||||
const nextTimeout = (timeout = 1000) => {
|
||||
setTimeout(this._pollStatus, timeout);
|
||||
};
|
||||
|
||||
this._store.dispatch(statusCollection({ isConnected, isConnecting, needsToken, secureToken }));
|
||||
const { isConnected, isConnecting, needsToken, secureToken } = this._api;
|
||||
|
||||
const apiStatus = {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
needsToken,
|
||||
secureToken
|
||||
};
|
||||
|
||||
const gotReconnected = !this._apiStatus.isConnected && apiStatus.isConnected;
|
||||
|
||||
if (gotReconnected) {
|
||||
this._pollLongStatus();
|
||||
}
|
||||
|
||||
if (!isEqual(apiStatus, this._apiStatus)) {
|
||||
this._store.dispatch(statusCollection(apiStatus));
|
||||
this._apiStatus = apiStatus;
|
||||
}
|
||||
|
||||
// Ping if necessary, otherwise stop pinging
|
||||
if (this._shouldPing()) {
|
||||
this._pollPing();
|
||||
} else {
|
||||
this._stopPollPing();
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
nextTimeout(250);
|
||||
return;
|
||||
return nextTimeout(250);
|
||||
}
|
||||
|
||||
const { refreshStatus } = this._store.getState().nodeStatus;
|
||||
|
||||
const statusPromises = [ this._api.eth.syncing() ];
|
||||
|
||||
if (refreshStatus) {
|
||||
statusPromises.push(this._api.eth.hashrate());
|
||||
statusPromises.push(this._api.parity.netPeers());
|
||||
}
|
||||
|
||||
Promise
|
||||
.all(statusPromises)
|
||||
.then((statusResults) => {
|
||||
const status = statusResults.length === 1
|
||||
? {
|
||||
syncing: statusResults[0]
|
||||
}
|
||||
: {
|
||||
syncing: statusResults[0],
|
||||
hashrate: statusResults[1],
|
||||
netPeers: statusResults[2]
|
||||
};
|
||||
|
||||
if (!isEqual(status, this._status)) {
|
||||
this._store.dispatch(statusCollection(status));
|
||||
this._status = status;
|
||||
}
|
||||
|
||||
nextTimeout();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('_pollStatus', error);
|
||||
nextTimeout(250);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Miner settings should never changes unless
|
||||
* Parity is restarted, or if the values are changed
|
||||
* from the UI
|
||||
*/
|
||||
_pollMinerSettings = () => {
|
||||
Promise
|
||||
.all([
|
||||
this._api.eth.coinbase(),
|
||||
this._api.parity.extraData(),
|
||||
this._api.parity.minGasPrice(),
|
||||
this._api.parity.gasFloorTarget()
|
||||
])
|
||||
.then(([
|
||||
coinbase, extraData, minGasPrice, gasFloorTarget
|
||||
]) => {
|
||||
const minerSettings = {
|
||||
coinbase,
|
||||
extraData,
|
||||
minGasPrice,
|
||||
gasFloorTarget
|
||||
};
|
||||
|
||||
if (!isEqual(minerSettings, this._minerSettings)) {
|
||||
this._store.dispatch(statusCollection(minerSettings));
|
||||
this._minerSettings = minerSettings;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('_pollMinerSettings', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The data fetched here should not change
|
||||
* unless Parity is restarted. They are thus
|
||||
* fetched every 30s just in case, and whenever
|
||||
* the client got reconnected.
|
||||
*/
|
||||
_pollLongStatus = () => {
|
||||
const nextTimeout = (timeout = 30000) => {
|
||||
if (this._longStatusTimeoutId) {
|
||||
clearTimeout(this._longStatusTimeoutId);
|
||||
}
|
||||
|
||||
this._longStatusTimeoutId = setTimeout(this._pollLongStatus, timeout);
|
||||
};
|
||||
|
||||
// Poll Miner settings just in case
|
||||
this._pollMinerSettings();
|
||||
|
||||
Promise
|
||||
.all([
|
||||
this._api.web3.clientVersion(),
|
||||
this._api.eth.coinbase(),
|
||||
this._api.parity.defaultExtraData(),
|
||||
this._api.parity.extraData(),
|
||||
this._api.parity.gasFloorTarget(),
|
||||
this._api.eth.hashrate(),
|
||||
this._api.parity.minGasPrice(),
|
||||
this._api.parity.netChain(),
|
||||
this._api.parity.netPeers(),
|
||||
this._api.parity.netPort(),
|
||||
this._api.parity.nodeName(),
|
||||
this._api.parity.rpcSettings(),
|
||||
this._api.eth.syncing()
|
||||
this._api.parity.enode()
|
||||
])
|
||||
.then(([clientVersion, coinbase, defaultExtraData, extraData, gasFloorTarget, hashrate, minGasPrice, netChain, netPeers, netPort, nodeName, rpcSettings, syncing, traceMode]) => {
|
||||
.then(([
|
||||
clientVersion, defaultExtraData, netChain, netPort, rpcSettings, enode
|
||||
]) => {
|
||||
const isTest = netChain === 'morden' || netChain === 'testnet';
|
||||
|
||||
this._store.dispatch(statusCollection({
|
||||
const longStatus = {
|
||||
clientVersion,
|
||||
coinbase,
|
||||
defaultExtraData,
|
||||
extraData,
|
||||
gasFloorTarget,
|
||||
hashrate,
|
||||
minGasPrice,
|
||||
netChain,
|
||||
netPeers,
|
||||
netPort,
|
||||
nodeName,
|
||||
rpcSettings,
|
||||
syncing,
|
||||
isTest,
|
||||
traceMode
|
||||
}));
|
||||
enode,
|
||||
isTest
|
||||
};
|
||||
|
||||
if (!isEqual(longStatus, this._longStatus)) {
|
||||
this._store.dispatch(statusCollection(longStatus));
|
||||
this._longStatus = longStatus;
|
||||
}
|
||||
|
||||
nextTimeout();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('_pollStatus', error);
|
||||
console.error('_pollLongStatus', error);
|
||||
nextTimeout(250);
|
||||
});
|
||||
|
||||
nextTimeout();
|
||||
}
|
||||
|
||||
_pollLogs = () => {
|
||||
|
@ -47,3 +47,10 @@ export function clearStatusLogs () {
|
||||
type: 'clearStatusLogs'
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleStatusRefresh (refreshStatus) {
|
||||
return {
|
||||
type: 'toggleStatusRefresh',
|
||||
refreshStatus
|
||||
};
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ const initialState = {
|
||||
enode: '',
|
||||
extraData: '',
|
||||
gasFloorTarget: new BigNumber(0),
|
||||
gasLimit: new BigNumber(0),
|
||||
hashrate: new BigNumber(0),
|
||||
minGasPrice: new BigNumber(0),
|
||||
netChain: 'morden',
|
||||
@ -37,12 +38,13 @@ const initialState = {
|
||||
max: new BigNumber(0)
|
||||
},
|
||||
netPort: new BigNumber(0),
|
||||
nodeName: '',
|
||||
rpcSettings: {},
|
||||
syncing: false,
|
||||
isApiConnected: true,
|
||||
isPingConnected: true,
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
isPingable: false,
|
||||
isTest: false,
|
||||
refreshStatus: false,
|
||||
traceMode: undefined
|
||||
};
|
||||
|
||||
@ -73,5 +75,10 @@ export default handleActions({
|
||||
|
||||
clearStatusLogs (state, action) {
|
||||
return Object.assign({}, state, { devLogs: [] });
|
||||
},
|
||||
|
||||
toggleStatusRefresh (state, action) {
|
||||
const { refreshStatus } = action;
|
||||
return Object.assign({}, state, { refreshStatus });
|
||||
}
|
||||
}, initialState);
|
||||
|
17
js/src/ui/Form/RadioButtons/index.js
Normal file
17
js/src/ui/Form/RadioButtons/index.js
Normal 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 './radioButtons';
|
32
js/src/ui/Form/RadioButtons/radioButtons.css
Normal file
32
js/src/ui/Form/RadioButtons/radioButtons.css
Normal file
@ -0,0 +1,32 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
.spaced {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.typeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.desc {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #ccc;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
100
js/src/ui/Form/RadioButtons/radioButtons.js
Normal file
100
js/src/ui/Form/RadioButtons/radioButtons.js
Normal file
@ -0,0 +1,100 @@
|
||||
// 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 { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||
|
||||
import styles from './radioButtons.css';
|
||||
|
||||
export default class RadioButtons extends Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
values: PropTypes.array.isRequired,
|
||||
|
||||
value: PropTypes.any,
|
||||
name: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: 0,
|
||||
name: ''
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, values } = this.props;
|
||||
|
||||
const index = parseInt(value);
|
||||
const selectedValue = typeof value !== 'object' ? values[index] : value;
|
||||
const key = this.getKey(selectedValue, index);
|
||||
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
valueSelected={ key }
|
||||
name={ name }
|
||||
onChange={ this.onChange }
|
||||
>
|
||||
{ this.renderContent() }
|
||||
</RadioButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
const { values } = this.props;
|
||||
|
||||
return values.map((value, index) => {
|
||||
const label = typeof value === 'string' ? value : value.label || '';
|
||||
const description = (typeof value !== 'string' && value.description) || null;
|
||||
const key = this.getKey(value, index);
|
||||
|
||||
return (
|
||||
<RadioButton
|
||||
className={ styles.spaced }
|
||||
key={ index }
|
||||
|
||||
value={ key }
|
||||
label={ (
|
||||
<div className={ styles.typeContainer }>
|
||||
<span>{ label }</span>
|
||||
{
|
||||
description
|
||||
? (
|
||||
<span className={ styles.desc }>{ description }</span>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getKey (value, index) {
|
||||
if (typeof value !== 'string') {
|
||||
return typeof value.key === 'undefined' ? index : value.key;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
onChange = (event, index) => {
|
||||
const { onChange, values } = this.props;
|
||||
|
||||
const value = values[index] || values.find((v) => v.key === index);
|
||||
onChange(value, index);
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import InputAddressSelect from './InputAddressSelect';
|
||||
import InputChip from './InputChip';
|
||||
import InputInline from './InputInline';
|
||||
import Select from './Select';
|
||||
import RadioButtons from './RadioButtons';
|
||||
|
||||
export default from './form';
|
||||
export {
|
||||
@ -34,5 +35,6 @@ export {
|
||||
InputAddressSelect,
|
||||
InputChip,
|
||||
InputInline,
|
||||
Select
|
||||
Select,
|
||||
RadioButtons
|
||||
};
|
||||
|
@ -43,7 +43,7 @@ class Modal extends Component {
|
||||
waiting: PropTypes.array,
|
||||
scroll: PropTypes.bool,
|
||||
steps: PropTypes.array,
|
||||
title: React.PropTypes.oneOfType([
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.node, PropTypes.string
|
||||
]),
|
||||
visible: PropTypes.bool.isRequired,
|
||||
|
@ -19,5 +19,5 @@
|
||||
}
|
||||
|
||||
.layout>div {
|
||||
padding-bottom: 0.25em;
|
||||
padding-bottom: 0.75em;
|
||||
}
|
||||
|
@ -31,9 +31,14 @@ class TxHash extends Component {
|
||||
static propTypes = {
|
||||
hash: PropTypes.string.isRequired,
|
||||
isTest: PropTypes.bool,
|
||||
summary: PropTypes.bool
|
||||
summary: PropTypes.bool,
|
||||
maxConfirmations: PropTypes.number
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
maxConfirmations: 10
|
||||
};
|
||||
|
||||
state = {
|
||||
blockNumber: new BigNumber(0),
|
||||
transaction: null,
|
||||
@ -79,6 +84,7 @@ class TxHash extends Component {
|
||||
}
|
||||
|
||||
renderConfirmations () {
|
||||
const { maxConfirmations } = this.props;
|
||||
const { blockNumber, transaction } = this.state;
|
||||
|
||||
let txBlock = 'Pending';
|
||||
@ -89,14 +95,16 @@ class TxHash extends Component {
|
||||
const num = blockNumber.minus(transaction.blockNumber).plus(1);
|
||||
txBlock = `#${transaction.blockNumber.toFormat(0)}`;
|
||||
confirmations = num.toFormat(0);
|
||||
value = num.gt(10) ? 10 : num.toNumber();
|
||||
value = num.gt(maxConfirmations) ? maxConfirmations : num.toNumber();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.confirm }>
|
||||
<LinearProgress
|
||||
className={ styles.progressbar }
|
||||
min={ 0 } max={ 10 } value={ value }
|
||||
min={ 0 }
|
||||
max={ maxConfirmations }
|
||||
value={ value }
|
||||
color='white'
|
||||
mode='determinate' />
|
||||
<div className={ styles.progressinfo }>
|
||||
|
@ -29,7 +29,7 @@ import ContextProvider from './ContextProvider';
|
||||
import CopyToClipboard from './CopyToClipboard';
|
||||
import Editor from './Editor';
|
||||
import Errors from './Errors';
|
||||
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select } from './Form';
|
||||
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
|
||||
import IdentityIcon from './IdentityIcon';
|
||||
import IdentityName from './IdentityName';
|
||||
import MethodDecoding from './MethodDecoding';
|
||||
@ -78,6 +78,7 @@ export {
|
||||
muiTheme,
|
||||
Page,
|
||||
ParityBackground,
|
||||
RadioButtons,
|
||||
SignerIcon,
|
||||
Tags,
|
||||
Tooltip,
|
||||
|
28
js/src/util/check-if-tx-failed.js
Normal file
28
js/src/util/check-if-tx-failed.js
Normal file
@ -0,0 +1,28 @@
|
||||
// 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/>.
|
||||
|
||||
const checkIfTxFailed = (api, tx, gasSent) => {
|
||||
return api.pollMethod('eth_getTransactionReceipt', tx)
|
||||
.then((receipt) => {
|
||||
// TODO: Right now, there's no way to tell wether the EVM code crashed.
|
||||
// Because you usually send a bit more gas than estimated (to make sure
|
||||
// it gets mined quickly), we transaction probably failed if all the gas
|
||||
// has been used up.
|
||||
return receipt.gasUsed.eq(gasSent);
|
||||
});
|
||||
};
|
||||
|
||||
export default checkIfTxFailed;
|
21
js/src/util/nullable-proptype.js
Normal file
21
js/src/util/nullable-proptype.js
Normal file
@ -0,0 +1,21 @@
|
||||
// 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 { PropTypes } from 'react';
|
||||
|
||||
export default function (type) {
|
||||
return PropTypes.oneOfType([ PropTypes.oneOf([ null ]), type ]);
|
||||
}
|
@ -37,7 +37,7 @@ export function validateAbi (abi, api) {
|
||||
try {
|
||||
abiParsed = JSON.parse(abi);
|
||||
|
||||
if (!api.util.isArray(abiParsed) || !abiParsed.length) {
|
||||
if (!api.util.isArray(abiParsed)) {
|
||||
abiError = ERRORS.invalidAbi;
|
||||
return { abi, abiError, abiParsed };
|
||||
}
|
||||
|
44
js/src/util/wait-for-block-confirmations.js
Normal file
44
js/src/util/wait-for-block-confirmations.js
Normal file
@ -0,0 +1,44 @@
|
||||
// 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/>.
|
||||
|
||||
const isValidReceipt = (receipt) => {
|
||||
return receipt && receipt.blockNumber && receipt.blockNumber.gt(0);
|
||||
};
|
||||
|
||||
const waitForConfirmations = (api, tx, confirmations) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
api.pollMethod('eth_getTransactionReceipt', tx, isValidReceipt)
|
||||
.then((receipt) => {
|
||||
let subscription;
|
||||
api.subscribe('eth_blockNumber', (err, block) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (block.minus(confirmations - 1).gte(receipt.blockNumber)) {
|
||||
if (subscription) {
|
||||
api.unsubscribe(subscription);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.then((_subscription) => {
|
||||
subscription = _subscription;
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default waitForConfirmations;
|
@ -20,8 +20,9 @@ import { bindActionCreators } from 'redux';
|
||||
import ContentCreate from 'material-ui/svg-icons/content/create';
|
||||
import ContentSend from 'material-ui/svg-icons/content/send';
|
||||
import LockIcon from 'material-ui/svg-icons/action/lock';
|
||||
import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
|
||||
|
||||
import { EditMeta, Shapeshift, Transfer, PasswordManager } from '../../modals';
|
||||
import { EditMeta, Shapeshift, SMSVerification, Transfer, PasswordManager } from '../../modals';
|
||||
import { Actionbar, Button, Page } from '../../ui';
|
||||
|
||||
import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png';
|
||||
@ -29,9 +30,15 @@ import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png';
|
||||
import Header from './Header';
|
||||
import Transactions from './Transactions';
|
||||
|
||||
import VerificationStore from '../../modals/SMSVerification/store';
|
||||
|
||||
import styles from './account.css';
|
||||
|
||||
class Account extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object,
|
||||
accounts: PropTypes.object,
|
||||
@ -45,10 +52,20 @@ class Account extends Component {
|
||||
state = {
|
||||
showEditDialog: false,
|
||||
showFundDialog: false,
|
||||
showVerificationDialog: false,
|
||||
verificationStore: null,
|
||||
showTransferDialog: false,
|
||||
showPasswordDialog: false
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { api } = this.context;
|
||||
const { address } = this.props.params;
|
||||
|
||||
const store = new VerificationStore(api, address);
|
||||
this.setState({ verificationStore: store });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accounts, balances, isTest } = this.props;
|
||||
const { address } = this.props.params;
|
||||
@ -64,6 +81,7 @@ class Account extends Component {
|
||||
<div className={ styles.account }>
|
||||
{ this.renderEditDialog(account) }
|
||||
{ this.renderFundDialog() }
|
||||
{ this.renderVerificationDialog() }
|
||||
{ this.renderTransferDialog() }
|
||||
{ this.renderPasswordDialog() }
|
||||
{ this.renderActionbar() }
|
||||
@ -99,6 +117,11 @@ class Account extends Component {
|
||||
icon={ <img src={ shapeshiftBtn } className={ styles.btnicon } /> }
|
||||
label='shapeshift'
|
||||
onClick={ this.onShapeshiftAccountClick } />,
|
||||
<Button
|
||||
key='sms-verification'
|
||||
icon={ <VerifyIcon /> }
|
||||
label='Verify'
|
||||
onClick={ this.openVerification } />,
|
||||
<Button
|
||||
key='editmeta'
|
||||
icon={ <ContentCreate /> }
|
||||
@ -149,6 +172,22 @@ class Account extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderVerificationDialog () {
|
||||
if (!this.state.showVerificationDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const store = this.state.verificationStore;
|
||||
const { address } = this.props.params;
|
||||
|
||||
return (
|
||||
<SMSVerification
|
||||
store={ store } account={ address }
|
||||
onClose={ this.onVerificationClose }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderTransferDialog () {
|
||||
const { showTransferDialog } = this.state;
|
||||
|
||||
@ -205,6 +244,14 @@ class Account extends Component {
|
||||
this.onShapeshiftAccountClick();
|
||||
}
|
||||
|
||||
openVerification = () => {
|
||||
this.setState({ showVerificationDialog: true });
|
||||
}
|
||||
|
||||
onVerificationClose = () => {
|
||||
this.setState({ showVerificationDialog: false });
|
||||
}
|
||||
|
||||
onTransferClick = () => {
|
||||
this.setState({
|
||||
showTransferDialog: !this.state.showTransferDialog
|
||||
|
@ -18,3 +18,22 @@
|
||||
.description {
|
||||
margin-top: .5em !important;
|
||||
}
|
||||
|
||||
.list {
|
||||
.background {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin: 0 -1.5em;
|
||||
padding: 0.5em 1.5em;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.byline {
|
||||
font-size: 0.75em;
|
||||
padding-top: 0.5em;
|
||||
line-height: 1.5em;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
@ -51,16 +51,37 @@ export default class AddDapps extends Component {
|
||||
] }
|
||||
visible
|
||||
scroll>
|
||||
<List>
|
||||
{ store.apps.map(this.renderApp) }
|
||||
</List>
|
||||
<div className={ styles.warning }>
|
||||
</div>
|
||||
{ this.renderList(store.sortedLocal, 'Applications locally available', 'All applications installed locally on the machine by the user for access by the Parity client.') }
|
||||
{ this.renderList(store.sortedBuiltin, 'Applications bundled with Parity', 'Experimental applications developed by the Parity team to show off dapp capabilities, integration, experimental features and to control certain network-wide client behaviour.') }
|
||||
{ this.renderList(store.sortedNetwork, 'Applications on the global network', 'These applications are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each application before interacting.') }
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderList (items, header, byline) {
|
||||
if (!items || !items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.list }>
|
||||
<div className={ styles.background }>
|
||||
<div className={ styles.header }>{ header }</div>
|
||||
<div className={ styles.byline }>{ byline }</div>
|
||||
</div>
|
||||
<List>
|
||||
{ items.map(this.renderApp) }
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderApp = (app) => {
|
||||
const { store } = this.props;
|
||||
const isHidden = store.hidden.includes(app.id);
|
||||
const isHidden = !store.displayApps[app.id].visible;
|
||||
|
||||
const onCheck = () => {
|
||||
if (isHidden) {
|
||||
store.showApp(app.id);
|
||||
|
@ -17,7 +17,7 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { Container, ContainerTitle } from '../../../ui';
|
||||
import { Container, ContainerTitle, Tags } from '../../../ui';
|
||||
|
||||
import styles from './summary.css';
|
||||
|
||||
@ -49,6 +49,7 @@ export default class Summary extends Component {
|
||||
return (
|
||||
<Container className={ styles.container }>
|
||||
{ image }
|
||||
<Tags tags={ [app.type] } />
|
||||
<div className={ styles.description }>
|
||||
<ContainerTitle
|
||||
className={ styles.title }
|
||||
|
@ -5,7 +5,8 @@
|
||||
"name": "Token Deployment",
|
||||
"description": "Deploy new basic tokens that you are able to send around",
|
||||
"author": "Parity Team <admin@ethcore.io>",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.0",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "0xd1adaede68d344519025e2ff574650cd99d3830fe6d274c7a7843cdc00e17938",
|
||||
@ -13,7 +14,8 @@
|
||||
"name": "Registry",
|
||||
"description": "A global registry of addresses on the network",
|
||||
"author": "Parity Team <admin@ethcore.io>",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.0",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "0x0a8048117e51e964628d0f2d26342b3cd915248b59bcce2721e1d05f5cfa2208",
|
||||
@ -21,7 +23,8 @@
|
||||
"name": "Token Registry",
|
||||
"description": "A registry of transactable tokens on the network",
|
||||
"author": "Parity Team <admin@ethcore.io>",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.0",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "0xf49089046f53f5d2e5f3513c1c32f5ff57d986e46309a42d2b249070e4e72c46",
|
||||
@ -29,7 +32,8 @@
|
||||
"name": "Method Registry",
|
||||
"description": "A registry of method signatures for lookups on transactions",
|
||||
"author": "Parity Team <admin@ethcore.io>",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.0",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "0x058740ee9a5a3fb9f1cfa10752baec87e09cc45cd7027fd54708271aca300c75",
|
||||
@ -38,6 +42,7 @@
|
||||
"description": "A mapping of GitHub URLs to hashes for use in contracts as references",
|
||||
"author": "Parity Team <admin@ethcore.io>",
|
||||
"version": "1.0.0",
|
||||
"visible": false,
|
||||
"secure": true
|
||||
},
|
||||
{
|
||||
|
@ -18,6 +18,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -0.125em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.list+.list {
|
||||
@ -29,3 +30,25 @@
|
||||
flex: 0 1 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
bottom: 0.5em;
|
||||
left: -0.125em;
|
||||
position: absolute;
|
||||
right: -0.125em;
|
||||
top: -0.25em;
|
||||
z-index: 100;
|
||||
padding: 1em;
|
||||
|
||||
.body {
|
||||
line-height: 1.5em;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
max-width: 980px;
|
||||
|
||||
&>div:first-child {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Checkbox } from 'material-ui';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { Actionbar, Page } from '../../ui';
|
||||
@ -37,6 +38,24 @@ export default class Dapps extends Component {
|
||||
store = new DappsStore(this.context.api);
|
||||
|
||||
render () {
|
||||
let externalOverlay = null;
|
||||
if (this.store.externalOverlayVisible) {
|
||||
externalOverlay = (
|
||||
<div className={ styles.overlay }>
|
||||
<div className={ styles.body }>
|
||||
<div>Applications made available on the network by 3rd-party authors are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each before interacting.</div>
|
||||
<div>
|
||||
<Checkbox
|
||||
className={ styles.accept }
|
||||
label='I understand that these applications are not affiliated with Parity'
|
||||
checked={ false }
|
||||
onCheck={ this.onClickAcceptExternal } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AddDapps store={ this.store } />
|
||||
@ -53,14 +72,27 @@ export default class Dapps extends Component {
|
||||
] }
|
||||
/>
|
||||
<Page>
|
||||
<div className={ styles.list }>
|
||||
{ this.store.visible.map(this.renderApp) }
|
||||
</div>
|
||||
{ this.renderList(this.store.visibleLocal) }
|
||||
{ this.renderList(this.store.visibleBuiltin) }
|
||||
{ this.renderList(this.store.visibleNetwork, externalOverlay) }
|
||||
</Page>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderList (items, overlay) {
|
||||
if (!items || !items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.list }>
|
||||
{ overlay }
|
||||
{ items.map(this.renderApp) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderApp = (app) => {
|
||||
return (
|
||||
<div
|
||||
@ -70,4 +102,8 @@ export default class Dapps extends Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onClickAcceptExternal = () => {
|
||||
this.store.closeExternalOverlay();
|
||||
}
|
||||
}
|
||||
|
@ -14,39 +14,65 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { action, computed, observable, transaction } from 'mobx';
|
||||
import store from 'store';
|
||||
|
||||
import Contracts from '../../contracts';
|
||||
import { hashToImageUrl } from '../../redux/util';
|
||||
|
||||
import builtinApps from './builtin.json';
|
||||
|
||||
const LS_KEY_HIDDEN = 'hiddenApps';
|
||||
const LS_KEY_EXTERNAL = 'externalApps';
|
||||
const LS_KEY_DISPLAY = 'displayApps';
|
||||
const LS_KEY_EXTERNAL_ACCEPT = 'acceptExternal';
|
||||
|
||||
export default class DappsStore {
|
||||
@observable apps = [];
|
||||
@observable externalApps = [];
|
||||
@observable hiddenApps = [];
|
||||
@observable displayApps = {};
|
||||
@observable modalOpen = false;
|
||||
@observable externalOverlayVisible = true;
|
||||
|
||||
constructor (api) {
|
||||
this._api = api;
|
||||
|
||||
this._readHiddenApps();
|
||||
this._readExternalApps();
|
||||
this.loadExternalOverlay();
|
||||
this.readDisplayApps();
|
||||
|
||||
this._fetchBuiltinApps();
|
||||
this._fetchLocalApps();
|
||||
this._fetchRegistryApps();
|
||||
Promise
|
||||
.all([
|
||||
this._fetchBuiltinApps(),
|
||||
this._fetchLocalApps(),
|
||||
this._fetchRegistryApps()
|
||||
])
|
||||
.then(this.writeDisplayApps);
|
||||
}
|
||||
|
||||
@computed get visible () {
|
||||
return this.apps
|
||||
.filter((app) => {
|
||||
return this.externalApps.includes(app.id) || !this.hiddenApps.includes(app.id);
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
@computed get sortedBuiltin () {
|
||||
return this.apps.filter((app) => app.type === 'builtin');
|
||||
}
|
||||
|
||||
@computed get sortedLocal () {
|
||||
return this.apps.filter((app) => app.type === 'local');
|
||||
}
|
||||
|
||||
@computed get sortedNetwork () {
|
||||
return this.apps.filter((app) => app.type === 'network');
|
||||
}
|
||||
|
||||
@computed get visibleApps () {
|
||||
return this.apps.filter((app) => this.displayApps[app.id] && this.displayApps[app.id].visible);
|
||||
}
|
||||
|
||||
@computed get visibleBuiltin () {
|
||||
return this.visibleApps.filter((app) => app.type === 'builtin');
|
||||
}
|
||||
|
||||
@computed get visibleLocal () {
|
||||
return this.visibleApps.filter((app) => app.type === 'local');
|
||||
}
|
||||
|
||||
@computed get visibleNetwork () {
|
||||
return this.visibleApps.filter((app) => app.type === 'network');
|
||||
}
|
||||
|
||||
@action openModal = () => {
|
||||
@ -57,14 +83,48 @@ export default class DappsStore {
|
||||
this.modalOpen = false;
|
||||
}
|
||||
|
||||
@action closeExternalOverlay = () => {
|
||||
this.externalOverlayVisible = false;
|
||||
store.set(LS_KEY_EXTERNAL_ACCEPT, true);
|
||||
}
|
||||
|
||||
@action loadExternalOverlay () {
|
||||
this.externalOverlayVisible = !(store.get(LS_KEY_EXTERNAL_ACCEPT) || false);
|
||||
}
|
||||
|
||||
@action hideApp = (id) => {
|
||||
this.hiddenApps = this.hiddenApps.concat(id);
|
||||
this._writeHiddenApps();
|
||||
this.displayApps = Object.assign({}, this.displayApps, { [id]: { visible: false } });
|
||||
this.writeDisplayApps();
|
||||
}
|
||||
|
||||
@action showApp = (id) => {
|
||||
this.hiddenApps = this.hiddenApps.filter((_id) => _id !== id);
|
||||
this._writeHiddenApps();
|
||||
this.displayApps = Object.assign({}, this.displayApps, { [id]: { visible: true } });
|
||||
this.writeDisplayApps();
|
||||
}
|
||||
|
||||
@action readDisplayApps = () => {
|
||||
this.displayApps = store.get(LS_KEY_DISPLAY) || {};
|
||||
}
|
||||
|
||||
@action writeDisplayApps = () => {
|
||||
store.set(LS_KEY_DISPLAY, this.displayApps);
|
||||
}
|
||||
|
||||
@action addApps = (apps) => {
|
||||
transaction(() => {
|
||||
this.apps = this.apps
|
||||
.concat(apps || [])
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const visibility = {};
|
||||
apps.forEach((app) => {
|
||||
if (!this.displayApps[app.id]) {
|
||||
visibility[app.id] = { visible: app.visible };
|
||||
}
|
||||
});
|
||||
|
||||
this.displayApps = Object.assign({}, this.displayApps, visibility);
|
||||
});
|
||||
}
|
||||
|
||||
_getHost (api) {
|
||||
@ -79,13 +139,16 @@ export default class DappsStore {
|
||||
return Promise
|
||||
.all(builtinApps.map((app) => dappReg.getImage(app.id)))
|
||||
.then((imageIds) => {
|
||||
transaction(() => {
|
||||
builtinApps.forEach((app, index) => {
|
||||
this.addApps(
|
||||
builtinApps.map((app, index) => {
|
||||
app.type = 'builtin';
|
||||
app.image = hashToImageUrl(imageIds[index]);
|
||||
this.apps.push(app);
|
||||
});
|
||||
});
|
||||
return app;
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('DappsStore:fetchBuiltinApps', error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -100,15 +163,12 @@ export default class DappsStore {
|
||||
return apps
|
||||
.map((app) => {
|
||||
app.type = 'local';
|
||||
app.visible = true;
|
||||
return app;
|
||||
})
|
||||
.filter((app) => app.id && !['ui'].includes(app.id));
|
||||
})
|
||||
.then((apps) => {
|
||||
transaction(() => {
|
||||
(apps || []).forEach((app) => this.apps.push(app));
|
||||
});
|
||||
})
|
||||
.then(this.addApps)
|
||||
.catch((error) => {
|
||||
console.warn('DappsStore:fetchLocal', error);
|
||||
});
|
||||
@ -132,7 +192,9 @@ export default class DappsStore {
|
||||
.then((appsInfo) => {
|
||||
const appIds = appsInfo
|
||||
.map(([appId, owner]) => this._api.util.bytesToHex(appId))
|
||||
.filter((appId) => !builtinApps.find((app) => app.id === appId));
|
||||
.filter((appId) => {
|
||||
return (new BigNumber(appId)).gt(0) && !builtinApps.find((app) => app.id === appId);
|
||||
});
|
||||
|
||||
return Promise
|
||||
.all([
|
||||
@ -147,7 +209,8 @@ export default class DappsStore {
|
||||
image: hashToImageUrl(imageIds[index]),
|
||||
contentHash: this._api.util.bytesToHex(contentIds[index]).substr(2),
|
||||
manifestHash: this._api.util.bytesToHex(manifestIds[index]).substr(2),
|
||||
type: 'network'
|
||||
type: 'network',
|
||||
visible: true
|
||||
};
|
||||
|
||||
return app;
|
||||
@ -179,11 +242,7 @@ export default class DappsStore {
|
||||
});
|
||||
});
|
||||
})
|
||||
.then((apps) => {
|
||||
transaction(() => {
|
||||
(apps || []).forEach((app) => this.apps.push(app));
|
||||
});
|
||||
})
|
||||
.then(this.addApps)
|
||||
.catch((error) => {
|
||||
console.warn('DappsStore:fetchRegistry', error);
|
||||
});
|
||||
@ -201,44 +260,4 @@ export default class DappsStore {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
_readHiddenApps () {
|
||||
const stored = localStorage.getItem(LS_KEY_HIDDEN);
|
||||
|
||||
if (stored) {
|
||||
try {
|
||||
this.hiddenApps = JSON.parse(stored);
|
||||
} catch (error) {
|
||||
console.warn('DappsStore:readHiddenApps', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_readExternalApps () {
|
||||
const stored = localStorage.getItem(LS_KEY_EXTERNAL);
|
||||
|
||||
if (stored) {
|
||||
try {
|
||||
this.externalApps = JSON.parse(stored);
|
||||
} catch (error) {
|
||||
console.warn('DappsStore:readExternalApps', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_writeExternalApps () {
|
||||
try {
|
||||
localStorage.setItem(LS_KEY_EXTERNAL, JSON.stringify(this.externalApps));
|
||||
} catch (error) {
|
||||
console.error('DappsStore:writeExternalApps', error);
|
||||
}
|
||||
}
|
||||
|
||||
_writeHiddenApps () {
|
||||
try {
|
||||
localStorage.setItem(LS_KEY_HIDDEN, JSON.stringify(this.hiddenApps));
|
||||
} catch (error) {
|
||||
console.error('DappsStore:writeHiddenApps', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import nullable from '../../../../util/nullable-proptype';
|
||||
|
||||
import Account from '../Account';
|
||||
import TransactionPendingForm from '../TransactionPendingForm';
|
||||
@ -22,8 +23,6 @@ import TxHashLink from '../TxHashLink';
|
||||
|
||||
import styles from './SignRequest.css';
|
||||
|
||||
const nullable = (type) => React.PropTypes.oneOfType([ React.PropTypes.oneOf([ null ]), type ]);
|
||||
|
||||
export default class SignRequest extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import nullable from '../../../../util/nullable-proptype';
|
||||
|
||||
import CircularProgress from 'material-ui/CircularProgress';
|
||||
|
||||
@ -29,8 +30,6 @@ import styles from './TransactionFinished.css';
|
||||
import * as tUtil from '../util/transaction';
|
||||
import { capitalize } from '../util/util';
|
||||
|
||||
const nullable = (type) => React.PropTypes.oneOfType([ React.PropTypes.oneOf([ null ]), type ]);
|
||||
|
||||
export default class TransactionFinished extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
|
@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { clearStatusLogs, toggleStatusLogs } from '../../../../redux/actions';
|
||||
import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from '../../../../redux/actions';
|
||||
|
||||
import Debug from '../../components/Debug';
|
||||
import Status from '../../components/Status';
|
||||
@ -31,6 +31,14 @@ class StatusPage extends Component {
|
||||
actions: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.actions.toggleStatusRefresh(true);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.actions.toggleStatusRefresh(false);
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={ styles.body }>
|
||||
@ -49,7 +57,8 @@ function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
clearStatusLogs,
|
||||
toggleStatusLogs
|
||||
toggleStatusLogs,
|
||||
toggleStatusRefresh
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
45
js/test/npmLibrary.js
Normal file
45
js/test/npmLibrary.js
Normal file
@ -0,0 +1,45 @@
|
||||
// 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/>.
|
||||
|
||||
try {
|
||||
var Api = require('../.npmjs/library.js').Api;
|
||||
var Abi = require('../.npmjs/library.js').Abi;
|
||||
|
||||
if (typeof Api !== 'function') {
|
||||
throw new Error('No Api');
|
||||
}
|
||||
|
||||
if (typeof Abi !== 'function') {
|
||||
throw new Error('No Abi');
|
||||
}
|
||||
|
||||
var transport = new Api.Transport.Http('http://localhost:8545');
|
||||
var api = new Api(transport);
|
||||
|
||||
api.eth
|
||||
.blockNumber()
|
||||
.then((block) => {
|
||||
console.log('library working fine', '(block #' + block.toFormat() + ')');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('library working fine (disconnected)');
|
||||
process.exit(0);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('An error occured:', e.toString().split('\n')[0]);
|
||||
process.exit(1);
|
||||
}
|
@ -34,7 +34,9 @@ module.exports = {
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, DEST),
|
||||
filename: '[name].js'
|
||||
filename: '[name].js',
|
||||
library: '[name].js',
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
|
@ -24,13 +24,23 @@ const isProd = ENV === 'production';
|
||||
|
||||
module.exports = {
|
||||
context: path.join(__dirname, './src'),
|
||||
target: 'node',
|
||||
entry: 'library.js',
|
||||
output: {
|
||||
path: path.join(__dirname, '.npmjs'),
|
||||
filename: 'library.js',
|
||||
libraryTarget: 'commonjs'
|
||||
library: 'Parity',
|
||||
libraryTarget: 'umd',
|
||||
umdNamedDefine: true
|
||||
},
|
||||
externals: {
|
||||
'node-fetch': 'node-fetch',
|
||||
'vertx': 'vertx'
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/babel-polyfill/
|
||||
],
|
||||
loaders: [
|
||||
{
|
||||
test: /(\.jsx|\.js)$/,
|
||||
|
@ -14,7 +14,7 @@ To temporarily disable Parity Wallet (and stop Parity) use:
|
||||
|
||||
To completely uninstall Parity Wallet use:
|
||||
|
||||
sudo -c /usr/local/libexec/uninstall-parity.sh
|
||||
sudo /usr/local/libexec/uninstall-parity.sh
|
||||
|
||||
|
||||
Parity is distributed under the terms of the GPL.
|
@ -28,6 +28,7 @@ use jsonrpc_core::Error;
|
||||
use v1::helpers::{errors, TransactionRequest, FilledTransactionRequest, ConfirmationPayload};
|
||||
use v1::types::{
|
||||
H256 as RpcH256, H520 as RpcH520, Bytes as RpcBytes,
|
||||
RichRawTransaction as RpcRichRawTransaction,
|
||||
ConfirmationPayload as RpcConfirmationPayload,
|
||||
ConfirmationResponse,
|
||||
SignRequest as RpcSignRequest,
|
||||
@ -47,8 +48,7 @@ pub fn execute<C, M>(client: &C, miner: &M, accounts: &AccountProvider, payload:
|
||||
},
|
||||
ConfirmationPayload::SignTransaction(request) => {
|
||||
sign_no_dispatch(client, miner, accounts, request, pass)
|
||||
.map(|tx| rlp::encode(&tx).to_vec())
|
||||
.map(RpcBytes)
|
||||
.map(RpcRichRawTransaction::from)
|
||||
.map(ConfirmationResponse::SignTransaction)
|
||||
},
|
||||
ConfirmationPayload::Signature(address, hash) => {
|
||||
|
@ -619,6 +619,10 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_transaction(&self, raw: Bytes) -> Result<RpcH256, Error> {
|
||||
self.send_raw_transaction(raw)
|
||||
}
|
||||
|
||||
fn call(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<Bytes, Error> {
|
||||
try!(self.active());
|
||||
|
||||
|
@ -34,6 +34,7 @@ use v1::traits::{EthSigning, ParitySigning};
|
||||
use v1::types::{
|
||||
H160 as RpcH160, H256 as RpcH256, U256 as RpcU256, Bytes as RpcBytes, H520 as RpcH520,
|
||||
Either as RpcEither,
|
||||
RichRawTransaction as RpcRichRawTransaction,
|
||||
TransactionRequest as RpcTransactionRequest,
|
||||
ConfirmationPayload as RpcConfirmationPayload,
|
||||
ConfirmationResponse as RpcConfirmationResponse
|
||||
@ -201,11 +202,11 @@ impl<C: 'static, M: 'static> EthSigning for SigningQueueClient<C, M> where
|
||||
});
|
||||
}
|
||||
|
||||
fn sign_transaction(&self, ready: Ready<RpcBytes>, request: RpcTransactionRequest) {
|
||||
fn sign_transaction(&self, ready: Ready<RpcRichRawTransaction>, request: RpcTransactionRequest) {
|
||||
let res = self.active().and_then(|_| self.dispatch(RpcConfirmationPayload::SignTransaction(request)));
|
||||
self.handle_dispatch(res, |response| {
|
||||
match response {
|
||||
Ok(RpcConfirmationResponse::SignTransaction(rlp)) => ready.ready(Ok(rlp)),
|
||||
Ok(RpcConfirmationResponse::SignTransaction(tx)) => ready.ready(Ok(tx)),
|
||||
Err(e) => ready.ready(Err(e)),
|
||||
e => ready.ready(Err(errors::internal("Unexpected result.", e))),
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ use v1::types::{
|
||||
U256 as RpcU256,
|
||||
H160 as RpcH160, H256 as RpcH256, H520 as RpcH520, Bytes as RpcBytes,
|
||||
Either as RpcEither,
|
||||
RichRawTransaction as RpcRichRawTransaction,
|
||||
TransactionRequest as RpcTransactionRequest,
|
||||
ConfirmationPayload as RpcConfirmationPayload,
|
||||
ConfirmationResponse as RpcConfirmationResponse,
|
||||
@ -100,9 +101,9 @@ impl<C: 'static, M: 'static> EthSigning for SigningUnsafeClient<C, M> where
|
||||
ready.ready(result);
|
||||
}
|
||||
|
||||
fn sign_transaction(&self, ready: Ready<RpcBytes>, request: RpcTransactionRequest) {
|
||||
fn sign_transaction(&self, ready: Ready<RpcRichRawTransaction>, request: RpcTransactionRequest) {
|
||||
let result = match self.handle(RpcConfirmationPayload::SignTransaction(request)) {
|
||||
Ok(RpcConfirmationResponse::SignTransaction(rlp)) => Ok(rlp),
|
||||
Ok(RpcConfirmationResponse::SignTransaction(tx)) => Ok(tx),
|
||||
Err(e) => Err(e),
|
||||
e => Err(errors::internal("Unexpected result", e)),
|
||||
};
|
||||
|
@ -18,8 +18,10 @@ use std::str::FromStr;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Instant, Duration};
|
||||
use rustc_serialize::hex::ToHex;
|
||||
use time::get_time;
|
||||
use rlp;
|
||||
use jsonrpc_core::IoHandler;
|
||||
|
||||
use util::{Uint, U256, Address, H256, FixedHash, Mutex};
|
||||
use ethcore::account_provider::AccountProvider;
|
||||
use ethcore::client::{TestBlockChainClient, EachBlockWith, Executed, TransactionID};
|
||||
@ -28,10 +30,10 @@ use ethcore::receipt::LocalizedReceipt;
|
||||
use ethcore::transaction::{Transaction, Action};
|
||||
use ethcore::miner::{ExternalMiner, MinerService};
|
||||
use ethsync::SyncState;
|
||||
|
||||
use jsonrpc_core::IoHandler;
|
||||
use v1::{Eth, EthClient, EthClientOptions, EthFilter, EthFilterClient, EthSigning, SigningUnsafeClient};
|
||||
use v1::tests::helpers::{TestSyncProvider, Config, TestMinerService, TestSnapshotService};
|
||||
use rustc_serialize::hex::ToHex;
|
||||
use time::get_time;
|
||||
|
||||
fn blockchain_client() -> Arc<TestBlockChainClient> {
|
||||
let client = TestBlockChainClient::new();
|
||||
@ -798,9 +800,25 @@ fn rpc_eth_sign_transaction() {
|
||||
};
|
||||
let signature = tester.accounts_provider.sign(address, None, t.hash(None)).unwrap();
|
||||
let t = t.with_signature(signature, None);
|
||||
let signature = t.signature();
|
||||
let rlp = rlp::encode(&t);
|
||||
|
||||
let response = r#"{"jsonrpc":"2.0","result":"0x"#.to_owned() + &rlp.to_hex() + r#"","id":1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","result":{"#.to_owned() +
|
||||
r#""raw":"0x"# + &rlp.to_hex() + r#"","# +
|
||||
r#""tx":{"# +
|
||||
r#""blockHash":null,"blockNumber":null,"creates":null,"# +
|
||||
&format!("\"from\":\"0x{:?}\",", &address) +
|
||||
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
|
||||
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
|
||||
r#""input":"0x","nonce":"0x1","# +
|
||||
&format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) +
|
||||
&format!("\"r\":\"0x{}\",", signature.r().to_hex()) +
|
||||
&format!("\"raw\":\"0x{}\",", rlp.to_hex()) +
|
||||
&format!("\"s\":\"0x{}\",", signature.s().to_hex()) +
|
||||
r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# +
|
||||
&format!("\"v\":{},", signature.v()) +
|
||||
r#""value":"0x9184e72a""# +
|
||||
r#"}},"id":1}"#;
|
||||
|
||||
tester.miner.last_nonces.write().insert(address.clone(), U256::zero());
|
||||
|
||||
|
@ -112,7 +112,7 @@ fn should_be_able_to_set_meta() {
|
||||
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_accountsInfo", "params": [], "id": 1}"#;
|
||||
let res = tester.io.handle_request_sync(request);
|
||||
let response = format!("{{\"jsonrpc\":\"2.0\",\"result\":{{\"0x{}\":{{\"meta\":\"{{foo: 69}}\",\"name\":\"{}\",\"uuid\":\"{}\"}}}},\"id\":1}}", address.hex(), uuid, uuid);
|
||||
let response = format!("{{\"jsonrpc\":\"2.0\",\"result\":{{\"0x{}\":{{\"meta\":\"{{foo: 69}}\",\"name\":\"\",\"uuid\":\"{}\"}}}},\"id\":1}}", address.hex(), uuid);
|
||||
assert_eq!(res, Some(response));
|
||||
}
|
||||
|
||||
|
@ -268,16 +268,32 @@ fn should_add_sign_transaction_to_the_queue() {
|
||||
};
|
||||
let signature = tester.accounts.sign(address, Some("test".into()), t.hash(None)).unwrap();
|
||||
let t = t.with_signature(signature, None);
|
||||
let signature = t.signature();
|
||||
let rlp = rlp::encode(&t);
|
||||
|
||||
let response = r#"{"jsonrpc":"2.0","result":"0x"#.to_owned() + &rlp.to_hex() + r#"","id":1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","result":{"#.to_owned() +
|
||||
r#""raw":"0x"# + &rlp.to_hex() + r#"","# +
|
||||
r#""tx":{"# +
|
||||
r#""blockHash":null,"blockNumber":null,"creates":null,"# +
|
||||
&format!("\"from\":\"0x{:?}\",", &address) +
|
||||
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
|
||||
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
|
||||
r#""input":"0x","nonce":"0x1","# +
|
||||
&format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) +
|
||||
&format!("\"r\":\"0x{}\",", signature.r().to_hex()) +
|
||||
&format!("\"raw\":\"0x{}\",", rlp.to_hex()) +
|
||||
&format!("\"s\":\"0x{}\",", signature.s().to_hex()) +
|
||||
r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# +
|
||||
&format!("\"v\":{},", signature.v()) +
|
||||
r#""value":"0x9184e72a""# +
|
||||
r#"}},"id":1}"#;
|
||||
|
||||
// then
|
||||
tester.miner.last_nonces.write().insert(address.clone(), U256::zero());
|
||||
let async_result = tester.io.handle_request(&request).unwrap();
|
||||
assert_eq!(tester.signer.requests().len(), 1);
|
||||
// respond
|
||||
tester.signer.request_confirmed(1.into(), Ok(ConfirmationResponse::SignTransaction(rlp.to_vec().into())));
|
||||
tester.signer.request_confirmed(1.into(), Ok(ConfirmationResponse::SignTransaction(t.into())));
|
||||
assert!(async_result.on_result(move |res| {
|
||||
assert_eq!(res, response.to_owned());
|
||||
}));
|
||||
|
@ -102,6 +102,10 @@ build_rpc_trait! {
|
||||
#[rpc(name = "eth_sendRawTransaction")]
|
||||
fn send_raw_transaction(&self, Bytes) -> Result<H256, Error>;
|
||||
|
||||
/// Alias of `eth_sendRawTransaction`.
|
||||
#[rpc(name = "eth_submitTransaction")]
|
||||
fn submit_transaction(&self, Bytes) -> Result<H256, Error>;
|
||||
|
||||
/// Call contract, returning the output data.
|
||||
#[rpc(name = "eth_call")]
|
||||
fn call(&self, CallRequest, Trailing<BlockNumber>) -> Result<Bytes, Error>;
|
||||
|
@ -17,7 +17,7 @@
|
||||
//! Eth rpc interface.
|
||||
|
||||
use v1::helpers::auto_args::{WrapAsync, Ready};
|
||||
use v1::types::{H160, H256, H520, TransactionRequest, Bytes};
|
||||
use v1::types::{H160, H256, H520, TransactionRequest, RichRawTransaction};
|
||||
|
||||
build_rpc_trait! {
|
||||
/// Signing methods implementation relying on unlocked accounts.
|
||||
@ -33,9 +33,9 @@ build_rpc_trait! {
|
||||
fn send_transaction(&self, Ready<H256>, TransactionRequest);
|
||||
|
||||
/// Signs transactions without dispatching it to the network.
|
||||
/// Returns signed transaction RLP representation.
|
||||
/// It can be later submitted using `eth_sendRawTransaction`.
|
||||
/// Returns signed transaction RLP representation and the transaction itself.
|
||||
/// It can be later submitted using `eth_sendRawTransaction/eth_submitTransaction`.
|
||||
#[rpc(async, name = "eth_signTransaction")]
|
||||
fn sign_transaction(&self, Ready<Bytes>, TransactionRequest);
|
||||
fn sign_transaction(&self, Ready<RichRawTransaction>, TransactionRequest);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
use std::fmt;
|
||||
use serde::{Serialize, Serializer};
|
||||
use v1::types::{U256, TransactionRequest, H160, H256, H520, Bytes};
|
||||
use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, Bytes};
|
||||
use v1::helpers;
|
||||
|
||||
/// Confirmation waiting in a queue
|
||||
@ -76,12 +76,12 @@ impl From<(H160, Bytes)> for DecryptRequest {
|
||||
}
|
||||
|
||||
/// Confirmation response for particular payload
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ConfirmationResponse {
|
||||
/// Transaction Hash
|
||||
SendTransaction(H256),
|
||||
/// Transaction RLP
|
||||
SignTransaction(Bytes),
|
||||
SignTransaction(RichRawTransaction),
|
||||
/// Signature
|
||||
Signature(H520),
|
||||
/// Decrypted data
|
||||
|
@ -44,7 +44,7 @@ pub use self::hash::{H64, H160, H256, H512, H520, H2048};
|
||||
pub use self::index::Index;
|
||||
pub use self::log::Log;
|
||||
pub use self::sync::{SyncStatus, SyncInfo, Peers, PeerInfo, PeerNetworkInfo, PeerProtocolsInfo, PeerEthereumProtocolInfo, TransactionStats};
|
||||
pub use self::transaction::{Transaction, LocalTransactionStatus};
|
||||
pub use self::transaction::{Transaction, RichRawTransaction, LocalTransactionStatus};
|
||||
pub use self::transaction_request::TransactionRequest;
|
||||
pub use self::receipt::Receipt;
|
||||
pub use self::rpc_settings::RpcSettings;
|
||||
|
@ -22,7 +22,7 @@ use v1::helpers::errors;
|
||||
use v1::types::{Bytes, H160, H256, U256, H512};
|
||||
|
||||
/// Transaction
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
|
||||
pub struct Transaction {
|
||||
/// Hash
|
||||
pub hash: H256,
|
||||
@ -132,6 +132,26 @@ impl Serialize for LocalTransactionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Geth-compatible output for eth_signTransaction method
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
|
||||
pub struct RichRawTransaction {
|
||||
/// Raw transaction RLP
|
||||
pub raw: Bytes,
|
||||
/// Transaction details
|
||||
#[serde(rename="tx")]
|
||||
pub transaction: Transaction
|
||||
}
|
||||
|
||||
impl From<SignedTransaction> for RichRawTransaction {
|
||||
fn from(t: SignedTransaction) -> Self {
|
||||
let tx: Transaction = t.into();
|
||||
RichRawTransaction {
|
||||
raw: tx.raw.clone(),
|
||||
transaction: tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalizedTransaction> for Transaction {
|
||||
fn from(t: LocalizedTransaction) -> Transaction {
|
||||
let signature = t.signature();
|
||||
|
@ -33,6 +33,8 @@ const MAX_BODIES_TO_REQUEST: usize = 64;
|
||||
const MAX_RECEPITS_TO_REQUEST: usize = 128;
|
||||
const SUBCHAIN_SIZE: u64 = 256;
|
||||
const MAX_ROUND_PARENTS: usize = 32;
|
||||
const MAX_PARALLEL_SUBCHAIN_DOWNLOAD: usize = 5;
|
||||
const MAX_REORG_BLOCKS: u64 = 20;
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
/// Downloader state
|
||||
@ -62,6 +64,14 @@ pub enum BlockRequest {
|
||||
},
|
||||
}
|
||||
|
||||
/// Indicates sync action
|
||||
pub enum DownloadAction {
|
||||
/// Do nothing
|
||||
None,
|
||||
/// Reset downloads for all peers
|
||||
Reset
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Debug)]
|
||||
pub enum BlockDownloaderImportError {
|
||||
/// Imported data is rejected as invalid.
|
||||
@ -175,11 +185,11 @@ impl BlockDownloader {
|
||||
}
|
||||
|
||||
/// Add new block headers.
|
||||
pub fn import_headers(&mut self, io: &mut SyncIo, r: &UntrustedRlp, expected_hash: Option<H256>) -> Result<(), BlockDownloaderImportError> {
|
||||
pub fn import_headers(&mut self, io: &mut SyncIo, r: &UntrustedRlp, expected_hash: Option<H256>) -> Result<DownloadAction, BlockDownloaderImportError> {
|
||||
let item_count = r.item_count();
|
||||
if self.state == State::Idle {
|
||||
trace!(target: "sync", "Ignored unexpected block headers");
|
||||
return Ok(())
|
||||
return Ok(DownloadAction::None)
|
||||
}
|
||||
if item_count == 0 && (self.state == State::Blocks) {
|
||||
return Err(BlockDownloaderImportError::Invalid);
|
||||
@ -188,6 +198,7 @@ impl BlockDownloader {
|
||||
let mut headers = Vec::new();
|
||||
let mut hashes = Vec::new();
|
||||
let mut valid_response = item_count == 0; //empty response is valid
|
||||
let mut any_known = false;
|
||||
for i in 0..item_count {
|
||||
let info: BlockHeader = try!(r.val_at(i).map_err(|e| {
|
||||
trace!(target: "sync", "Error decoding block header RLP: {:?}", e);
|
||||
@ -200,6 +211,7 @@ impl BlockDownloader {
|
||||
valid_response = expected == info.hash()
|
||||
}
|
||||
}
|
||||
any_known = any_known || self.blocks.contains_head(&info.hash());
|
||||
if self.blocks.contains(&info.hash()) {
|
||||
trace!(target: "sync", "Skipping existing block header {} ({:?})", number, info.hash());
|
||||
continue;
|
||||
@ -245,17 +257,23 @@ impl BlockDownloader {
|
||||
trace!(target: "sync", "Received {} subchain heads, proceeding to download", headers.len());
|
||||
self.blocks.reset_to(hashes);
|
||||
self.state = State::Blocks;
|
||||
return Ok(DownloadAction::Reset);
|
||||
}
|
||||
},
|
||||
State::Blocks => {
|
||||
let count = headers.len();
|
||||
// At least one of the heades must advance the subchain. Otherwise they are all useless.
|
||||
if count == 0 || !any_known {
|
||||
trace!(target: "sync", "No useful headers");
|
||||
return Err(BlockDownloaderImportError::Useless);
|
||||
}
|
||||
self.blocks.insert_headers(headers);
|
||||
trace!(target: "sync", "Inserted {} headers", count);
|
||||
},
|
||||
_ => trace!(target: "sync", "Unexpected headers({})", headers.len()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(DownloadAction::None)
|
||||
}
|
||||
|
||||
/// Called by peer once it has new block bodies
|
||||
@ -324,14 +342,21 @@ impl BlockDownloader {
|
||||
self.last_imported_hash = p.clone();
|
||||
trace!(target: "sync", "Searching common header from the last round {} ({})", self.last_imported_block, self.last_imported_hash);
|
||||
} else {
|
||||
match io.chain().block_hash(BlockID::Number(self.last_imported_block - 1)) {
|
||||
Some(h) => {
|
||||
self.last_imported_block -= 1;
|
||||
self.last_imported_hash = h;
|
||||
trace!(target: "sync", "Searching common header in the blockchain {} ({})", self.last_imported_block, self.last_imported_hash);
|
||||
}
|
||||
None => {
|
||||
debug!(target: "sync", "Could not revert to previous block, last: {} ({})", self.last_imported_block, self.last_imported_hash);
|
||||
let best = io.chain().chain_info().best_block_number;
|
||||
if best > self.last_imported_block && best - self.last_imported_block > MAX_REORG_BLOCKS {
|
||||
debug!(target: "sync", "Could not revert to previous ancient block, last: {} ({})", self.last_imported_block, self.last_imported_hash);
|
||||
self.reset();
|
||||
} else {
|
||||
match io.chain().block_hash(BlockID::Number(self.last_imported_block - 1)) {
|
||||
Some(h) => {
|
||||
self.last_imported_block -= 1;
|
||||
self.last_imported_hash = h;
|
||||
trace!(target: "sync", "Searching common header in the blockchain {} ({})", self.last_imported_block, self.last_imported_hash);
|
||||
}
|
||||
None => {
|
||||
debug!(target: "sync", "Could not revert to previous block, last: {} ({})", self.last_imported_block, self.last_imported_hash);
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -342,22 +367,26 @@ impl BlockDownloader {
|
||||
}
|
||||
|
||||
/// Find some headers or blocks to download for a peer.
|
||||
pub fn request_blocks(&mut self, io: &mut SyncIo) -> Option<BlockRequest> {
|
||||
pub fn request_blocks(&mut self, io: &mut SyncIo, num_active_peers: usize) -> Option<BlockRequest> {
|
||||
match self.state {
|
||||
State::Idle => {
|
||||
self.start_sync_round(io);
|
||||
return self.request_blocks(io);
|
||||
if self.state == State::ChainHead {
|
||||
return self.request_blocks(io, num_active_peers);
|
||||
}
|
||||
},
|
||||
State::ChainHead => {
|
||||
// Request subchain headers
|
||||
trace!(target: "sync", "Starting sync with better chain");
|
||||
// Request MAX_HEADERS_TO_REQUEST - 2 headers apart so that
|
||||
// MAX_HEADERS_TO_REQUEST would include headers for neighbouring subchains
|
||||
return Some(BlockRequest::Headers {
|
||||
start: self.last_imported_hash.clone(),
|
||||
count: SUBCHAIN_SIZE,
|
||||
skip: (MAX_HEADERS_TO_REQUEST - 2) as u64,
|
||||
});
|
||||
if num_active_peers < MAX_PARALLEL_SUBCHAIN_DOWNLOAD {
|
||||
// Request subchain headers
|
||||
trace!(target: "sync", "Starting sync with better chain");
|
||||
// Request MAX_HEADERS_TO_REQUEST - 2 headers apart so that
|
||||
// MAX_HEADERS_TO_REQUEST would include headers for neighbouring subchains
|
||||
return Some(BlockRequest::Headers {
|
||||
start: self.last_imported_hash.clone(),
|
||||
count: SUBCHAIN_SIZE,
|
||||
skip: (MAX_HEADERS_TO_REQUEST - 2) as u64,
|
||||
});
|
||||
}
|
||||
},
|
||||
State::Blocks => {
|
||||
// check to see if we need to download any block bodies first
|
||||
|
@ -301,11 +301,16 @@ impl BlockCollection {
|
||||
self.heads.len() == 0 || (self.heads.len() == 1 && self.head.map_or(false, |h| h == self.heads[0]))
|
||||
}
|
||||
|
||||
/// Chech is collection contains a block header.
|
||||
/// Check if collection contains a block header.
|
||||
pub fn contains(&self, hash: &H256) -> bool {
|
||||
self.blocks.contains_key(hash)
|
||||
}
|
||||
|
||||
/// Check if collection contains a block header.
|
||||
pub fn contains_head(&self, hash: &H256) -> bool {
|
||||
self.heads.contains(hash)
|
||||
}
|
||||
|
||||
/// Return used heap size.
|
||||
pub fn heap_size(&self) -> usize {
|
||||
self.heads.heap_size_of_children()
|
||||
|
@ -37,7 +37,7 @@
|
||||
/// Workflow for `ChainHead` state.
|
||||
/// In this state we try to get subchain headers with a single `GetBlockHeaders` request.
|
||||
/// On `NewPeer` / On `Restart`:
|
||||
/// If peer's total difficulty is higher, request N/M headers with interval M+1 starting from l
|
||||
/// If peer's total difficulty is higher and there are less than 5 peers downloading, request N/M headers with interval M+1 starting from l
|
||||
/// On `BlockHeaders(R)`:
|
||||
/// If R is empty:
|
||||
/// If l is equal to genesis block hash or l is more than 1000 blocks behind our best hash:
|
||||
@ -49,8 +49,8 @@
|
||||
/// Else
|
||||
/// Set S to R, set s to `Blocks`.
|
||||
///
|
||||
///
|
||||
/// All other messages are ignored.
|
||||
///
|
||||
/// Workflow for `Blocks` state.
|
||||
/// In this state we download block headers and bodies from multiple peers.
|
||||
/// On `NewPeer` / On `Restart`:
|
||||
@ -62,7 +62,9 @@
|
||||
///
|
||||
/// On `BlockHeaders(R)`:
|
||||
/// If R is empty remove current peer from P and restart.
|
||||
/// Validate received headers. For each header find a parent in H or R or the blockchain. Restart if there is a block with unknown parent.
|
||||
/// Validate received headers:
|
||||
/// For each header find a parent in H or R or the blockchain. Restart if there is a block with unknown parent.
|
||||
/// Find at least one header from the received list in S. Restart if there is none.
|
||||
/// Go to `CollectBlocks`.
|
||||
///
|
||||
/// On `BlockBodies(R)`:
|
||||
@ -98,7 +100,7 @@ use ethcore::snapshot::{ManifestData, RestorationStatus};
|
||||
use sync_io::SyncIo;
|
||||
use time;
|
||||
use super::SyncConfig;
|
||||
use block_sync::{BlockDownloader, BlockRequest, BlockDownloaderImportError as DownloaderImportError};
|
||||
use block_sync::{BlockDownloader, BlockRequest, BlockDownloaderImportError as DownloaderImportError, DownloadAction};
|
||||
use snapshot::{Snapshot, ChunkType};
|
||||
use rand::{thread_rng, Rng};
|
||||
use api::{PeerInfo as PeerInfoDigest, WARP_SYNC_PROTOCOL_ID};
|
||||
@ -307,6 +309,15 @@ impl PeerInfo {
|
||||
fn is_allowed(&self) -> bool {
|
||||
self.confirmation != ForkConfirmation::Unconfirmed && !self.expired
|
||||
}
|
||||
|
||||
fn reset_asking(&mut self) {
|
||||
self.asking_blocks.clear();
|
||||
self.asking_hash = None;
|
||||
// mark any pending requests as expired
|
||||
if self.asking != PeerAsking::Nothing && self.is_allowed() {
|
||||
self.expired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Blockchain sync handler.
|
||||
@ -434,12 +445,7 @@ impl ChainSync {
|
||||
}
|
||||
for (_, ref mut p) in &mut self.peers {
|
||||
if p.block_set != Some(BlockSet::OldBlocks) {
|
||||
p.asking_blocks.clear();
|
||||
p.asking_hash = None;
|
||||
// mark any pending requests as expired
|
||||
if p.asking != PeerAsking::Nothing && p.is_allowed() {
|
||||
p.expired = true;
|
||||
}
|
||||
p.reset_asking();
|
||||
}
|
||||
}
|
||||
self.state = SyncState::Idle;
|
||||
@ -650,8 +656,9 @@ impl ChainSync {
|
||||
|
||||
self.clear_peer_download(peer_id);
|
||||
let expected_hash = self.peers.get(&peer_id).and_then(|p| p.asking_hash);
|
||||
let allowed = self.peers.get(&peer_id).map(|p| p.is_allowed()).unwrap_or(false);
|
||||
let block_set = self.peers.get(&peer_id).and_then(|p| p.block_set).unwrap_or(BlockSet::NewBlocks);
|
||||
if !self.reset_peer_asking(peer_id, PeerAsking::BlockHeaders) || expected_hash.is_none() {
|
||||
if !self.reset_peer_asking(peer_id, PeerAsking::BlockHeaders) || expected_hash.is_none() || !allowed {
|
||||
trace!(target: "sync", "{}: Ignored unexpected headers, expected_hash = {:?}", peer_id, expected_hash);
|
||||
self.continue_sync(io);
|
||||
return Ok(());
|
||||
@ -696,7 +703,15 @@ impl ChainSync {
|
||||
self.continue_sync(io);
|
||||
return Ok(());
|
||||
},
|
||||
Ok(()) => (),
|
||||
Ok(DownloadAction::Reset) => {
|
||||
// mark all outstanding requests as expired
|
||||
trace!("Resetting downloads for {:?}", block_set);
|
||||
for (_, ref mut p) in self.peers.iter_mut().filter(|&(_, ref p)| p.block_set == Some(block_set)) {
|
||||
p.reset_asking();
|
||||
}
|
||||
|
||||
}
|
||||
Ok(DownloadAction::None) => {},
|
||||
}
|
||||
|
||||
self.collect_blocks(io, block_set);
|
||||
@ -988,7 +1003,7 @@ impl ChainSync {
|
||||
return Ok(());
|
||||
}
|
||||
self.clear_peer_download(peer_id);
|
||||
if !self.reset_peer_asking(peer_id, PeerAsking::SnapshotData) || self.state != SyncState::SnapshotData {
|
||||
if !self.reset_peer_asking(peer_id, PeerAsking::SnapshotData) || (self.state != SyncState::SnapshotData && self.state != SyncState::SnapshotWaiting) {
|
||||
trace!(target: "sync", "{}: Ignored unexpected snapshot data", peer_id);
|
||||
self.continue_sync(io);
|
||||
return Ok(());
|
||||
@ -1120,6 +1135,7 @@ impl ChainSync {
|
||||
};
|
||||
let chain_info = io.chain().chain_info();
|
||||
let syncing_difficulty = chain_info.pending_total_difficulty;
|
||||
let num_active_peers = self.peers.values().filter(|p| p.asking != PeerAsking::Nothing).count();
|
||||
|
||||
let higher_difficulty = peer_difficulty.map_or(true, |pd| pd > syncing_difficulty);
|
||||
if force || self.state == SyncState::NewBlocks || higher_difficulty || self.old_blocks.is_some() {
|
||||
@ -1137,7 +1153,8 @@ impl ChainSync {
|
||||
let have_latest = io.chain().block_status(BlockID::Hash(peer_latest)) != BlockStatus::Unknown;
|
||||
if !have_latest && (higher_difficulty || force || self.state == SyncState::NewBlocks) {
|
||||
// check if got new blocks to download
|
||||
if let Some(request) = self.new_blocks.request_blocks(io) {
|
||||
trace!(target: "sync", "Syncing with {}, force={}, td={:?}, our td={}, state={:?}", peer_id, force, peer_difficulty, syncing_difficulty, self.state);
|
||||
if let Some(request) = self.new_blocks.request_blocks(io, num_active_peers) {
|
||||
self.request_blocks(io, peer_id, request, BlockSet::NewBlocks);
|
||||
if self.state == SyncState::Idle {
|
||||
self.state = SyncState::Blocks;
|
||||
@ -1146,7 +1163,7 @@ impl ChainSync {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(request) = self.old_blocks.as_mut().and_then(|d| d.request_blocks(io)) {
|
||||
if let Some(request) = self.old_blocks.as_mut().and_then(|d| d.request_blocks(io, num_active_peers)) {
|
||||
self.request_blocks(io, peer_id, request, BlockSet::OldBlocks);
|
||||
return;
|
||||
}
|
||||
|
@ -79,14 +79,14 @@ fn empty_blocks() {
|
||||
fn forked() {
|
||||
::env_logger::init().ok();
|
||||
let mut net = TestNet::new(3);
|
||||
net.peer_mut(0).chain.add_blocks(300, EachBlockWith::Uncle);
|
||||
net.peer_mut(1).chain.add_blocks(300, EachBlockWith::Uncle);
|
||||
net.peer_mut(2).chain.add_blocks(300, EachBlockWith::Uncle);
|
||||
net.peer_mut(0).chain.add_blocks(100, EachBlockWith::Nothing); //fork
|
||||
net.peer_mut(1).chain.add_blocks(200, EachBlockWith::Uncle);
|
||||
net.peer_mut(2).chain.add_blocks(200, EachBlockWith::Uncle);
|
||||
net.peer_mut(1).chain.add_blocks(100, EachBlockWith::Uncle); //fork between 1 and 2
|
||||
net.peer_mut(2).chain.add_blocks(10, EachBlockWith::Nothing);
|
||||
net.peer_mut(0).chain.add_blocks(30, EachBlockWith::Uncle);
|
||||
net.peer_mut(1).chain.add_blocks(30, EachBlockWith::Uncle);
|
||||
net.peer_mut(2).chain.add_blocks(30, EachBlockWith::Uncle);
|
||||
net.peer_mut(0).chain.add_blocks(10, EachBlockWith::Nothing); //fork
|
||||
net.peer_mut(1).chain.add_blocks(20, EachBlockWith::Uncle);
|
||||
net.peer_mut(2).chain.add_blocks(20, EachBlockWith::Uncle);
|
||||
net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Uncle); //fork between 1 and 2
|
||||
net.peer_mut(2).chain.add_blocks(1, EachBlockWith::Nothing);
|
||||
// peer 1 has the best chain of 601 blocks
|
||||
let peer1_chain = net.peer(1).chain.numbers.read().clone();
|
||||
net.sync();
|
||||
@ -102,12 +102,12 @@ fn forked_with_misbehaving_peer() {
|
||||
let mut net = TestNet::new(3);
|
||||
// peer 0 is on a totally different chain with higher total difficulty
|
||||
net.peer_mut(0).chain = TestBlockChainClient::new_with_extra_data(b"fork".to_vec());
|
||||
net.peer_mut(0).chain.add_blocks(500, EachBlockWith::Nothing);
|
||||
net.peer_mut(1).chain.add_blocks(100, EachBlockWith::Nothing);
|
||||
net.peer_mut(2).chain.add_blocks(100, EachBlockWith::Nothing);
|
||||
net.peer_mut(0).chain.add_blocks(50, EachBlockWith::Nothing);
|
||||
net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Nothing);
|
||||
net.peer_mut(2).chain.add_blocks(10, EachBlockWith::Nothing);
|
||||
|
||||
net.peer_mut(1).chain.add_blocks(100, EachBlockWith::Nothing);
|
||||
net.peer_mut(2).chain.add_blocks(200, EachBlockWith::Uncle);
|
||||
net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Nothing);
|
||||
net.peer_mut(2).chain.add_blocks(20, EachBlockWith::Uncle);
|
||||
// peer 1 should sync to peer 2, others should not change
|
||||
let peer0_chain = net.peer(0).chain.numbers.read().clone();
|
||||
let peer2_chain = net.peer(2).chain.numbers.read().clone();
|
||||
|
Loading…
Reference in New Issue
Block a user