In-browser signing support (#3231)
* Signer RAW confirmations * Returning address book as eth_accounts * UI support for in-browser signing * Post review fixes * Adding new methods to jsonrpc * Fixing eth_accounts * Deterministic accounts ordering
This commit is contained in:
parent
90ff810e36
commit
2f98169539
@ -95,6 +95,7 @@ impl KeyDirectory for NullDir {
|
|||||||
struct AddressBook {
|
struct AddressBook {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
cache: HashMap<Address, AccountMeta>,
|
cache: HashMap<Address, AccountMeta>,
|
||||||
|
transient: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddressBook {
|
impl AddressBook {
|
||||||
@ -106,11 +107,18 @@ impl AddressBook {
|
|||||||
let mut r = AddressBook {
|
let mut r = AddressBook {
|
||||||
path: path,
|
path: path,
|
||||||
cache: HashMap::new(),
|
cache: HashMap::new(),
|
||||||
|
transient: false,
|
||||||
};
|
};
|
||||||
r.revert();
|
r.revert();
|
||||||
r
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn transient() -> Self {
|
||||||
|
let mut book = AddressBook::new(Default::default());
|
||||||
|
book.transient = true;
|
||||||
|
book
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self) -> HashMap<Address, AccountMeta> {
|
pub fn get(&self) -> HashMap<Address, AccountMeta> {
|
||||||
self.cache.clone()
|
self.cache.clone()
|
||||||
}
|
}
|
||||||
@ -134,6 +142,7 @@ impl AddressBook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn revert(&mut self) {
|
fn revert(&mut self) {
|
||||||
|
if self.transient { return; }
|
||||||
trace!(target: "addressbook", "revert");
|
trace!(target: "addressbook", "revert");
|
||||||
let _ = fs::File::open(self.path.clone())
|
let _ = fs::File::open(self.path.clone())
|
||||||
.map_err(|e| trace!(target: "addressbook", "Couldn't open address book: {}", e))
|
.map_err(|e| trace!(target: "addressbook", "Couldn't open address book: {}", e))
|
||||||
@ -144,6 +153,7 @@ impl AddressBook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save(&mut self) {
|
fn save(&mut self) {
|
||||||
|
if self.transient { return; }
|
||||||
trace!(target: "addressbook", "save");
|
trace!(target: "addressbook", "save");
|
||||||
let _ = fs::File::create(self.path.clone())
|
let _ = fs::File::create(self.path.clone())
|
||||||
.map_err(|e| warn!(target: "addressbook", "Couldn't open address book for writing: {}", e))
|
.map_err(|e| warn!(target: "addressbook", "Couldn't open address book for writing: {}", e))
|
||||||
@ -175,7 +185,7 @@ impl AccountProvider {
|
|||||||
pub fn transient_provider() -> Self {
|
pub fn transient_provider() -> Self {
|
||||||
AccountProvider {
|
AccountProvider {
|
||||||
unlocked: Mutex::new(HashMap::new()),
|
unlocked: Mutex::new(HashMap::new()),
|
||||||
address_book: Mutex::new(AddressBook::new(Default::default())),
|
address_book: Mutex::new(AddressBook::transient()),
|
||||||
sstore: Box::new(EthStore::open(Box::new(NullDir::default()))
|
sstore: Box::new(EthStore::open(Box::new(NullDir::default()))
|
||||||
.expect("NullDir load always succeeds; qed"))
|
.expect("NullDir load always succeeds; qed"))
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,7 @@
|
|||||||
"bytes": "^2.4.0",
|
"bytes": "^2.4.0",
|
||||||
"chart.js": "^2.3.0",
|
"chart.js": "^2.3.0",
|
||||||
"es6-promise": "^3.2.1",
|
"es6-promise": "^3.2.1",
|
||||||
|
"ethereumjs-tx": "^1.1.2",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"format-json": "^1.0.3",
|
"format-json": "^1.0.3",
|
||||||
"format-number": "^2.0.1",
|
"format-number": "^2.0.1",
|
||||||
@ -147,6 +148,7 @@
|
|||||||
"redux-actions": "^0.10.1",
|
"redux-actions": "^0.10.1",
|
||||||
"redux-thunk": "^2.1.0",
|
"redux-thunk": "^2.1.0",
|
||||||
"rlp": "^2.0.0",
|
"rlp": "^2.0.0",
|
||||||
|
"scryptsy": "^2.0.0",
|
||||||
"store": "^1.3.20",
|
"store": "^1.3.20",
|
||||||
"utf8": "^2.1.1",
|
"utf8": "^2.1.1",
|
||||||
"validator": "^5.7.0",
|
"validator": "^5.7.0",
|
||||||
|
@ -93,6 +93,10 @@ export function inFilter (options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function inHex (str) {
|
export function inHex (str) {
|
||||||
|
if (str && str.toString) {
|
||||||
|
str = str.toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
if (str && str.substr(0, 2) === '0x') {
|
if (str && str.substr(0, 2) === '0x') {
|
||||||
return str.toLowerCase();
|
return str.toLowerCase();
|
||||||
}
|
}
|
||||||
|
@ -181,6 +181,12 @@ export default class Parity {
|
|||||||
.then(outAddress);
|
.then(outAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextNonce (account) {
|
||||||
|
return this._transport
|
||||||
|
.execute('parity_nextNonce', inAddress(account))
|
||||||
|
.then(outNumber);
|
||||||
|
}
|
||||||
|
|
||||||
nodeName () {
|
nodeName () {
|
||||||
return this._transport
|
return this._transport
|
||||||
.execute('parity_nodeName');
|
.execute('parity_nodeName');
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { inNumber16 } from '../../format/input';
|
import { inNumber16, inData } from '../../format/input';
|
||||||
import { outSignerRequest } from '../../format/output';
|
import { outSignerRequest } from '../../format/output';
|
||||||
|
|
||||||
export default class Signer {
|
export default class Signer {
|
||||||
@ -27,6 +27,11 @@ export default class Signer {
|
|||||||
.execute('signer_confirmRequest', inNumber16(requestId), options, password);
|
.execute('signer_confirmRequest', inNumber16(requestId), options, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirmRequestRaw (requestId, data) {
|
||||||
|
return this._transport
|
||||||
|
.execute('signer_confirmRequestRaw', inNumber16(requestId), inData(data));
|
||||||
|
}
|
||||||
|
|
||||||
generateAuthorizationToken () {
|
generateAuthorizationToken () {
|
||||||
return this._transport
|
return this._transport
|
||||||
.execute('signer_generateAuthorizationToken');
|
.execute('signer_generateAuthorizationToken');
|
||||||
|
@ -356,6 +356,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
nextNonce: {
|
||||||
|
desc: 'Returns next available nonce for transaction from given account. Includes pending block and transaction queue.',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
type: Address,
|
||||||
|
desc: 'Account'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: Quantity,
|
||||||
|
desc: 'Next valid nonce'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
nodeName: {
|
nodeName: {
|
||||||
desc: 'Returns node name (identity)',
|
desc: 'Returns node name (identity)',
|
||||||
params: [],
|
params: [],
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { Quantity } from '../types';
|
import { Quantity, Data } from '../types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
generateAuthorizationToken: {
|
generateAuthorizationToken: {
|
||||||
@ -57,6 +57,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
confirmRequestRaw: {
|
||||||
|
desc: 'Confirm a request in the signer queue providing signed request.',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
type: Quantity,
|
||||||
|
desc: 'The request id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: Data,
|
||||||
|
desc: 'Signed request (transaction RLP)'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: Boolean,
|
||||||
|
desc: 'The status of the confirmation'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
rejectRequest: {
|
rejectRequest: {
|
||||||
desc: 'Rejects a request in the signer queue',
|
desc: 'Rejects a request in the signer queue',
|
||||||
params: [
|
params: [
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
|
|
||||||
import * as actions from './signerActions';
|
import * as actions from './signerActions';
|
||||||
|
|
||||||
|
import { inHex } from '../../api/format/input';
|
||||||
|
import { Wallet } from '../../util/wallet';
|
||||||
|
|
||||||
export default class SignerMiddleware {
|
export default class SignerMiddleware {
|
||||||
constructor (api) {
|
constructor (api) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
@ -49,10 +52,10 @@ export default class SignerMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onConfirmStart = (store, action) => {
|
onConfirmStart = (store, action) => {
|
||||||
const { id, password } = action.payload;
|
const { id, password, wallet, payload } = action.payload;
|
||||||
|
|
||||||
this._api.signer
|
const handlePromise = promise => {
|
||||||
.confirmRequest(id, {}, password)
|
promise
|
||||||
.then((txHash) => {
|
.then((txHash) => {
|
||||||
console.log('confirmRequest', id, txHash);
|
console.log('confirmRequest', id, txHash);
|
||||||
if (!txHash) {
|
if (!txHash) {
|
||||||
@ -66,6 +69,41 @@ export default class SignerMiddleware {
|
|||||||
console.error('confirmRequest', id, error);
|
console.error('confirmRequest', id, error);
|
||||||
store.dispatch(actions.errorConfirmRequest({ id, err: error.message }));
|
store.dispatch(actions.errorConfirmRequest({ id, err: error.message }));
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign request in-browser
|
||||||
|
if (wallet && payload.transaction) {
|
||||||
|
const { transaction } = payload;
|
||||||
|
|
||||||
|
(transaction.nonce.isZero()
|
||||||
|
? this._api.parity.nextNonce(transaction.from)
|
||||||
|
: Promise.resolve(transaction.nonce)
|
||||||
|
).then(nonce => {
|
||||||
|
let txData = {
|
||||||
|
to: inHex(transaction.to),
|
||||||
|
nonce: inHex(transaction.nonce.isZero() ? nonce : transaction.nonce),
|
||||||
|
gasPrice: inHex(transaction.gasPrice),
|
||||||
|
gasLimit: inHex(transaction.gas),
|
||||||
|
value: inHex(transaction.value),
|
||||||
|
data: inHex(transaction.data)
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// NOTE: Derving the key takes significant amount of time,
|
||||||
|
// make sure to display some kind of "in-progress" state.
|
||||||
|
const signer = Wallet.fromJson(wallet, password);
|
||||||
|
const rawTx = signer.signTransaction(txData);
|
||||||
|
|
||||||
|
handlePromise(this._api.signer.confirmRequestRaw(id, rawTx));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
store.dispatch(actions.errorConfirmRequest({ id, err: error.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePromise(this._api.signer.confirmRequest(id, {}, password));
|
||||||
}
|
}
|
||||||
|
|
||||||
onRejectStart = (store, action) => {
|
onRejectStart = (store, action) => {
|
||||||
|
82
js/src/util/wallet.js
Normal file
82
js/src/util/wallet.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// 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 scrypt from 'scryptsy';
|
||||||
|
import Transaction from 'ethereumjs-tx';
|
||||||
|
import { pbkdf2Sync } from 'crypto';
|
||||||
|
import { createDecipheriv } from 'browserify-aes';
|
||||||
|
|
||||||
|
import { inHex } from '../api/format/input';
|
||||||
|
import { sha3 } from '../api/util/sha3';
|
||||||
|
|
||||||
|
// Adapted from https://github.com/kvhnuke/etherwallet/blob/mercury/app/scripts/myetherwallet.js
|
||||||
|
|
||||||
|
export class Wallet {
|
||||||
|
|
||||||
|
static fromJson (json, password) {
|
||||||
|
if (json.version !== 3) {
|
||||||
|
throw new Error('Only V3 wallets are supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { kdf } = json.crypto;
|
||||||
|
const kdfparams = json.crypto.kdfparams || {};
|
||||||
|
const pwd = Buffer.from(password);
|
||||||
|
const salt = Buffer.from(kdfparams.salt, 'hex');
|
||||||
|
let derivedKey;
|
||||||
|
|
||||||
|
if (kdf === 'scrypt') {
|
||||||
|
derivedKey = scrypt(pwd, salt, kdfparams.n, kdfparams.r, kdfparams.p, kdfparams.dklen);
|
||||||
|
} else if (kdf === 'pbkdf2') {
|
||||||
|
if (kdfparams.prf !== 'hmac-sha256') {
|
||||||
|
throw new Error('Unsupported parameters to PBKDF2');
|
||||||
|
}
|
||||||
|
derivedKey = pbkdf2Sync(pwd, salt, kdfparams.c, kdfparams.dklen, 'sha256');
|
||||||
|
} else {
|
||||||
|
throw new Error('Unsupported key derivation scheme');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ciphertext = Buffer.from(json.crypto.ciphertext, 'hex');
|
||||||
|
let mac = sha3(Buffer.concat([derivedKey.slice(16, 32), ciphertext]));
|
||||||
|
if (mac !== inHex(json.crypto.mac)) {
|
||||||
|
throw new Error('Key derivation failed - possibly wrong passphrase');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decipher = createDecipheriv(
|
||||||
|
json.crypto.cipher,
|
||||||
|
derivedKey.slice(0, 16),
|
||||||
|
Buffer.from(json.crypto.cipherparams.iv, 'hex')
|
||||||
|
);
|
||||||
|
let seed = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
||||||
|
|
||||||
|
while (seed.length < 32) {
|
||||||
|
const nullBuff = Buffer.from([0x00]);
|
||||||
|
seed = Buffer.concat([nullBuff, seed]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Wallet(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (seed) {
|
||||||
|
this.seed = seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
signTransaction (transaction) {
|
||||||
|
const tx = new Transaction(transaction);
|
||||||
|
tx.sign(this.seed);
|
||||||
|
return inHex(tx.serialize().toString('hex'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -33,15 +33,22 @@ export default class RequestPendingWeb3 extends Component {
|
|||||||
className: PropTypes.string
|
className: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onConfirm = data => {
|
||||||
|
const { onConfirm, payload } = this.props;
|
||||||
|
|
||||||
|
data.payload = payload;
|
||||||
|
onConfirm(data);
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { payload, id, className, isSending, date, onConfirm, onReject } = this.props;
|
const { payload, id, className, isSending, date, onReject } = this.props;
|
||||||
|
|
||||||
if (payload.sign) {
|
if (payload.sign) {
|
||||||
const { sign } = payload;
|
const { sign } = payload;
|
||||||
return (
|
return (
|
||||||
<SignRequest
|
<SignRequest
|
||||||
className={ className }
|
className={ className }
|
||||||
onConfirm={ onConfirm }
|
onConfirm={ this.onConfirm }
|
||||||
onReject={ onReject }
|
onReject={ onReject }
|
||||||
isSending={ isSending }
|
isSending={ isSending }
|
||||||
isFinished={ false }
|
isFinished={ false }
|
||||||
@ -57,7 +64,7 @@ export default class RequestPendingWeb3 extends Component {
|
|||||||
return (
|
return (
|
||||||
<TransactionPending
|
<TransactionPending
|
||||||
className={ className }
|
className={ className }
|
||||||
onConfirm={ onConfirm }
|
onConfirm={ this.onConfirm }
|
||||||
onReject={ onReject }
|
onReject={ onReject }
|
||||||
isSending={ isSending }
|
isSending={ isSending }
|
||||||
id={ id }
|
id={ id }
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
.mainContainer > * {
|
.mainContainer > * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
min-height: 120px;
|
min-height: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputs {
|
.inputs {
|
||||||
|
@ -116,9 +116,11 @@ export default class TransactionPending extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfirm = password => {
|
onConfirm = data => {
|
||||||
const { id, gasPrice } = this.props;
|
const { id, gasPrice } = this.props;
|
||||||
this.props.onConfirm({ id, password, gasPrice });
|
const { password, wallet } = data;
|
||||||
|
|
||||||
|
this.props.onConfirm({ id, password, wallet, gasPrice });
|
||||||
}
|
}
|
||||||
|
|
||||||
onReject = () => {
|
onReject = () => {
|
||||||
|
@ -40,3 +40,7 @@
|
|||||||
.passwordHint span {
|
.passwordHint span {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fileInput input {
|
||||||
|
top: 22px;
|
||||||
|
}
|
||||||
|
@ -35,26 +35,33 @@ class TransactionPendingFormConfirm extends Component {
|
|||||||
id = Math.random(); // for tooltip
|
id = Math.random(); // for tooltip
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
walletError: null,
|
||||||
|
wallet: null,
|
||||||
password: ''
|
password: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accounts, address, isSending } = this.props;
|
const { accounts, address, isSending } = this.props;
|
||||||
const { password } = this.state;
|
const { password, walletError, wallet } = this.state;
|
||||||
const account = accounts[address] || {};
|
const account = accounts[address] || {};
|
||||||
|
const isExternal = !account.uuid;
|
||||||
|
|
||||||
const passwordHint = account.meta && account.meta.passwordHint
|
const passwordHint = account.meta && account.meta.passwordHint
|
||||||
? (<div><span>(hint) </span>{ account.meta.passwordHint }</div>)
|
? (<div><span>(hint) </span>{ account.meta.passwordHint }</div>)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const isWalletOk = !isExternal || (walletError === null && wallet !== null);
|
||||||
|
const keyInput = isExternal ? this.renderKeyInput() : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.confirmForm }>
|
<div className={ styles.confirmForm }>
|
||||||
<Form>
|
<Form>
|
||||||
|
{ keyInput }
|
||||||
<Input
|
<Input
|
||||||
onChange={ this.onModifyPassword }
|
onChange={ this.onModifyPassword }
|
||||||
onKeyDown={ this.onKeyDown }
|
onKeyDown={ this.onKeyDown }
|
||||||
label='Account Password'
|
label={ isExternal ? 'Key Password' : 'Account Password' }
|
||||||
hint='unlock the account'
|
hint={ isExternal ? 'decrypt the key' : 'unlock the account' }
|
||||||
type='password'
|
type='password'
|
||||||
value={ password } />
|
value={ password } />
|
||||||
<div className={ styles.passwordHint }>
|
<div className={ styles.passwordHint }>
|
||||||
@ -71,7 +78,7 @@ class TransactionPendingFormConfirm extends Component {
|
|||||||
className={ styles.confirmButton }
|
className={ styles.confirmButton }
|
||||||
fullWidth
|
fullWidth
|
||||||
primary
|
primary
|
||||||
disabled={ isSending }
|
disabled={ isSending || !isWalletOk }
|
||||||
icon={ <IdentityIcon address={ address } button className={ styles.signerIcon } /> }
|
icon={ <IdentityIcon address={ address } button className={ styles.signerIcon } /> }
|
||||||
label={ isSending ? 'Confirming...' : 'Confirm Transaction' }
|
label={ isSending ? 'Confirming...' : 'Confirm Transaction' }
|
||||||
/>
|
/>
|
||||||
@ -82,6 +89,20 @@ class TransactionPendingFormConfirm extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderKeyInput () {
|
||||||
|
const { walletError } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
className={ styles.fileInput }
|
||||||
|
onChange={ this.onKeySelect }
|
||||||
|
error={ walletError }
|
||||||
|
label='Select Local Key'
|
||||||
|
type='file'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderTooltip () {
|
renderTooltip () {
|
||||||
if (this.state.password.length) {
|
if (this.state.password.length) {
|
||||||
return;
|
return;
|
||||||
@ -94,6 +115,26 @@ class TransactionPendingFormConfirm extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeySelect = evt => {
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = e => {
|
||||||
|
try {
|
||||||
|
const wallet = JSON.parse(e.target.result);
|
||||||
|
this.setState({
|
||||||
|
walletError: null,
|
||||||
|
wallet: wallet
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({
|
||||||
|
walletError: 'Given wallet file is invalid.',
|
||||||
|
wallet: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fileReader.readAsText(evt.target.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
onModifyPassword = evt => {
|
onModifyPassword = evt => {
|
||||||
const password = evt.target.value;
|
const password = evt.target.value;
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -102,8 +143,11 @@ class TransactionPendingFormConfirm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onConfirm = () => {
|
onConfirm = () => {
|
||||||
const { password } = this.state;
|
const { password, wallet } = this.state;
|
||||||
this.props.onConfirm(password);
|
|
||||||
|
this.props.onConfirm({
|
||||||
|
password, wallet
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown = evt => {
|
onKeyDown = evt => {
|
||||||
|
@ -22,6 +22,7 @@ const DEST = process.env.BUILD_DEST || '.build';
|
|||||||
|
|
||||||
let modules = [
|
let modules = [
|
||||||
'babel-polyfill',
|
'babel-polyfill',
|
||||||
|
'browserify-aes', 'ethereumjs-tx', 'scryptsy',
|
||||||
'react', 'react-dom', 'react-redux', 'react-router',
|
'react', 'react-dom', 'react-redux', 'react-router',
|
||||||
'redux', 'redux-thunk', 'react-router-redux',
|
'redux', 'redux-thunk', 'react-router-redux',
|
||||||
'lodash', 'material-ui', 'moment', 'blockies'
|
'lodash', 'material-ui', 'moment', 'blockies'
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
//! RPC Error codes and error objects
|
//! RPC Error codes and error objects
|
||||||
|
|
||||||
macro_rules! rpc_unimplemented {
|
macro_rules! rpc_unimplemented {
|
||||||
() => (Err(::v1::helpers::errors::unimplemented()))
|
() => (Err(::v1::helpers::errors::unimplemented(None)))
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -51,11 +51,11 @@ mod codes {
|
|||||||
pub const FETCH_ERROR: i64 = -32060;
|
pub const FETCH_ERROR: i64 = -32060;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unimplemented() -> Error {
|
pub fn unimplemented(details: Option<String>) -> Error {
|
||||||
Error {
|
Error {
|
||||||
code: ErrorCode::ServerError(codes::UNSUPPORTED_REQUEST),
|
code: ErrorCode::ServerError(codes::UNSUPPORTED_REQUEST),
|
||||||
message: "This request is not implemented yet. Please create an issue on Github repo.".into(),
|
message: "This request is not implemented yet. Please create an issue on Github repo.".into(),
|
||||||
data: None
|
data: details.map(Value::String),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ extern crate ethash;
|
|||||||
|
|
||||||
use std::io::{Write};
|
use std::io::{Write};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Instant, Duration};
|
use std::time::{Instant, Duration};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
@ -339,7 +340,10 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
|
|||||||
|
|
||||||
let store = take_weak!(self.accounts);
|
let store = take_weak!(self.accounts);
|
||||||
let accounts = try!(store.accounts().map_err(|e| errors::internal("Could not fetch accounts.", e)));
|
let accounts = try!(store.accounts().map_err(|e| errors::internal("Could not fetch accounts.", e)));
|
||||||
Ok(accounts.into_iter().map(Into::into).collect())
|
let addresses = try!(store.addresses_info().map_err(|e| errors::internal("Could not fetch accounts.", e)));
|
||||||
|
|
||||||
|
let set: BTreeSet<Address> = accounts.into_iter().chain(addresses.keys().cloned()).collect();
|
||||||
|
Ok(set.into_iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn block_number(&self) -> Result<RpcU256, Error> {
|
fn block_number(&self) -> Result<RpcU256, Error> {
|
||||||
|
@ -18,14 +18,17 @@
|
|||||||
|
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
use jsonrpc_core::*;
|
use rlp::{UntrustedRlp, View};
|
||||||
use ethcore::account_provider::AccountProvider;
|
use ethcore::account_provider::AccountProvider;
|
||||||
use ethcore::client::MiningBlockChainClient;
|
use ethcore::client::MiningBlockChainClient;
|
||||||
|
use ethcore::transaction::SignedTransaction;
|
||||||
use ethcore::miner::MinerService;
|
use ethcore::miner::MinerService;
|
||||||
|
|
||||||
|
use jsonrpc_core::Error;
|
||||||
use v1::traits::Signer;
|
use v1::traits::Signer;
|
||||||
use v1::types::{TransactionModification, ConfirmationRequest, ConfirmationResponse, U256};
|
use v1::types::{TransactionModification, ConfirmationRequest, ConfirmationResponse, U256, Bytes};
|
||||||
use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload};
|
use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload};
|
||||||
use v1::helpers::dispatch;
|
use v1::helpers::dispatch::{self, dispatch_transaction};
|
||||||
|
|
||||||
/// Transactions confirmation (personal) rpc implementation.
|
/// Transactions confirmation (personal) rpc implementation.
|
||||||
pub struct SignerClient<C, M> where C: MiningBlockChainClient, M: MinerService {
|
pub struct SignerClient<C, M> where C: MiningBlockChainClient, M: MinerService {
|
||||||
@ -101,6 +104,60 @@ impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockC
|
|||||||
}).unwrap_or_else(|| Err(errors::invalid_params("Unknown RequestID", id)))
|
}).unwrap_or_else(|| Err(errors::invalid_params("Unknown RequestID", id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn confirm_request_raw(&self, id: U256, bytes: Bytes) -> Result<ConfirmationResponse, Error> {
|
||||||
|
try!(self.active());
|
||||||
|
|
||||||
|
let id = id.into();
|
||||||
|
let signer = take_weak!(self.signer);
|
||||||
|
let client = take_weak!(self.client);
|
||||||
|
let miner = take_weak!(self.miner);
|
||||||
|
|
||||||
|
signer.peek(&id).map(|confirmation| {
|
||||||
|
let result = match confirmation.payload {
|
||||||
|
ConfirmationPayload::SendTransaction(request) => {
|
||||||
|
let signed_transaction: SignedTransaction = try!(
|
||||||
|
UntrustedRlp::new(&bytes.0).as_val().map_err(errors::from_rlp_error)
|
||||||
|
);
|
||||||
|
let sender = try!(
|
||||||
|
signed_transaction.sender().map_err(|e| errors::invalid_params("Invalid signature.", e))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verification
|
||||||
|
let sender_matches = sender == request.from;
|
||||||
|
let data_matches = signed_transaction.data == request.data;
|
||||||
|
let value_matches = signed_transaction.value == request.value;
|
||||||
|
let nonce_matches = match request.nonce {
|
||||||
|
Some(nonce) => signed_transaction.nonce == nonce,
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dispatch if everything is ok
|
||||||
|
if sender_matches && data_matches && value_matches && nonce_matches {
|
||||||
|
dispatch_transaction(&*client, &*miner, signed_transaction)
|
||||||
|
.map(Into::into)
|
||||||
|
.map(ConfirmationResponse::SendTransaction)
|
||||||
|
} else {
|
||||||
|
let mut error = Vec::new();
|
||||||
|
if !sender_matches { error.push("from") }
|
||||||
|
if !data_matches { error.push("data") }
|
||||||
|
if !value_matches { error.push("value") }
|
||||||
|
if !nonce_matches { error.push("nonce") }
|
||||||
|
|
||||||
|
Err(errors::invalid_params("Sent transaction does not match the request.", error))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO [ToDr]:
|
||||||
|
// 1. Sign - verify signature
|
||||||
|
// 2. Decrypt - pass through?
|
||||||
|
_ => Err(errors::unimplemented(Some("Non-transaction requests does not support RAW signing yet.".into()))),
|
||||||
|
};
|
||||||
|
if let Ok(ref response) = result {
|
||||||
|
signer.request_confirmed(id, Ok(response.clone()));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}).unwrap_or_else(|| Err(errors::invalid_params("Unknown RequestID", id)))
|
||||||
|
}
|
||||||
|
|
||||||
fn reject_request(&self, id: U256) -> Result<bool, Error> {
|
fn reject_request(&self, id: U256) -> Result<bool, Error> {
|
||||||
try!(self.active());
|
try!(self.active());
|
||||||
let signer = take_weak!(self.signer);
|
let signer = take_weak!(self.signer);
|
||||||
|
@ -353,9 +353,16 @@ fn rpc_eth_gas_price() {
|
|||||||
fn rpc_eth_accounts() {
|
fn rpc_eth_accounts() {
|
||||||
let tester = EthTester::default();
|
let tester = EthTester::default();
|
||||||
let address = tester.accounts_provider.new_account("").unwrap();
|
let address = tester.accounts_provider.new_account("").unwrap();
|
||||||
|
let address2 = Address::default();
|
||||||
|
|
||||||
|
tester.accounts_provider.set_address_name(address2, "Test Account".into()).unwrap();
|
||||||
|
|
||||||
let request = r#"{"jsonrpc": "2.0", "method": "eth_accounts", "params": [], "id": 1}"#;
|
let request = r#"{"jsonrpc": "2.0", "method": "eth_accounts", "params": [], "id": 1}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","result":[""#.to_owned() + &format!("0x{:?}", address) + r#""],"id":1}"#;
|
let response = r#"{"jsonrpc":"2.0","result":[""#.to_owned()
|
||||||
|
+ &format!("0x{:?}", address2)
|
||||||
|
+ r#"",""#
|
||||||
|
+ &format!("0x{:?}", address)
|
||||||
|
+ r#""],"id":1}"#;
|
||||||
|
|
||||||
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
|
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,14 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use jsonrpc_core::IoHandler;
|
use util::{U256, Uint, Address, ToPretty};
|
||||||
use util::{U256, Uint, Address};
|
|
||||||
use ethcore::account_provider::AccountProvider;
|
use ethcore::account_provider::AccountProvider;
|
||||||
use ethcore::client::TestBlockChainClient;
|
use ethcore::client::TestBlockChainClient;
|
||||||
use ethcore::transaction::{Transaction, Action};
|
use ethcore::transaction::{Transaction, Action};
|
||||||
|
use rlp::encode;
|
||||||
|
|
||||||
|
use jsonrpc_core::IoHandler;
|
||||||
use v1::{SignerClient, Signer};
|
use v1::{SignerClient, Signer};
|
||||||
use v1::tests::helpers::TestMinerService;
|
use v1::tests::helpers::TestMinerService;
|
||||||
use v1::helpers::{SigningQueue, SignerService, FilledTransactionRequest, ConfirmationPayload};
|
use v1::helpers::{SigningQueue, SignerService, FilledTransactionRequest, ConfirmationPayload};
|
||||||
@ -206,6 +209,98 @@ fn should_confirm_transaction_and_dispatch() {
|
|||||||
assert_eq!(tester.miner.imported_transactions.lock().len(), 1);
|
assert_eq!(tester.miner.imported_transactions.lock().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_confirm_transaction_with_rlp() {
|
||||||
|
// given
|
||||||
|
let tester = signer_tester();
|
||||||
|
let address = tester.accounts.new_account("test").unwrap();
|
||||||
|
let recipient = Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap();
|
||||||
|
tester.signer.add_request(ConfirmationPayload::SendTransaction(FilledTransactionRequest {
|
||||||
|
from: address,
|
||||||
|
to: Some(recipient),
|
||||||
|
gas_price: U256::from(10_000),
|
||||||
|
gas: U256::from(10_000_000),
|
||||||
|
value: U256::from(1),
|
||||||
|
data: vec![],
|
||||||
|
nonce: None,
|
||||||
|
})).unwrap();
|
||||||
|
|
||||||
|
let t = Transaction {
|
||||||
|
nonce: U256::zero(),
|
||||||
|
gas_price: U256::from(0x1000),
|
||||||
|
gas: U256::from(10_000_000),
|
||||||
|
action: Action::Call(recipient),
|
||||||
|
value: U256::from(0x1),
|
||||||
|
data: vec![]
|
||||||
|
};
|
||||||
|
tester.accounts.unlock_account_temporarily(address, "test".into()).unwrap();
|
||||||
|
let signature = tester.accounts.sign(address, None, t.hash(None)).unwrap();
|
||||||
|
let t = t.with_signature(signature, None);
|
||||||
|
let rlp = encode(&t);
|
||||||
|
|
||||||
|
assert_eq!(tester.signer.requests().len(), 1);
|
||||||
|
|
||||||
|
// when
|
||||||
|
let request = r#"{
|
||||||
|
"jsonrpc":"2.0",
|
||||||
|
"method":"signer_confirmRequestRaw",
|
||||||
|
"params":["0x1", "0x"#.to_owned() + &rlp.to_hex() + r#""],
|
||||||
|
"id":1
|
||||||
|
}"#;
|
||||||
|
println!("{:?}", request);
|
||||||
|
let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#;
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(tester.io.handle_request_sync(&request), Some(response.to_owned()));
|
||||||
|
assert_eq!(tester.signer.requests().len(), 0);
|
||||||
|
assert_eq!(tester.miner.imported_transactions.lock().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_error_when_sender_does_not_match() {
|
||||||
|
// given
|
||||||
|
let tester = signer_tester();
|
||||||
|
let address = tester.accounts.new_account("test").unwrap();
|
||||||
|
let recipient = Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap();
|
||||||
|
tester.signer.add_request(ConfirmationPayload::SendTransaction(FilledTransactionRequest {
|
||||||
|
from: Address::default(),
|
||||||
|
to: Some(recipient),
|
||||||
|
gas_price: U256::from(10_000),
|
||||||
|
gas: U256::from(10_000_000),
|
||||||
|
value: U256::from(1),
|
||||||
|
data: vec![],
|
||||||
|
nonce: None,
|
||||||
|
})).unwrap();
|
||||||
|
|
||||||
|
let t = Transaction {
|
||||||
|
nonce: U256::zero(),
|
||||||
|
gas_price: U256::from(0x1000),
|
||||||
|
gas: U256::from(10_000_000),
|
||||||
|
action: Action::Call(recipient),
|
||||||
|
value: U256::from(0x1),
|
||||||
|
data: vec![]
|
||||||
|
};
|
||||||
|
tester.accounts.unlock_account_temporarily(address, "test".into()).unwrap();
|
||||||
|
let signature = tester.accounts.sign(address, None, t.hash(None)).unwrap();
|
||||||
|
let t = t.with_signature(signature, None);
|
||||||
|
let rlp = encode(&t);
|
||||||
|
|
||||||
|
assert_eq!(tester.signer.requests().len(), 1);
|
||||||
|
|
||||||
|
// when
|
||||||
|
let request = r#"{
|
||||||
|
"jsonrpc":"2.0",
|
||||||
|
"method":"signer_confirmRequestRaw",
|
||||||
|
"params":["0x1", "0x"#.to_owned() + &rlp.to_hex() + r#""],
|
||||||
|
"id":1
|
||||||
|
}"#;
|
||||||
|
let response = r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Couldn't parse parameters: Sent transaction does not match the request.","data":"[\"from\"]"},"id":1}"#;
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(tester.io.handle_request_sync(&request), Some(response.to_owned()));
|
||||||
|
assert_eq!(tester.signer.requests().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_generate_new_token() {
|
fn should_generate_new_token() {
|
||||||
// given
|
// given
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
use jsonrpc_core::Error;
|
use jsonrpc_core::Error;
|
||||||
|
|
||||||
use v1::helpers::auto_args::Wrap;
|
use v1::helpers::auto_args::Wrap;
|
||||||
use v1::types::{U256, TransactionModification, ConfirmationRequest, ConfirmationResponse};
|
use v1::types::{U256, Bytes, TransactionModification, ConfirmationRequest, ConfirmationResponse};
|
||||||
|
|
||||||
|
|
||||||
build_rpc_trait! {
|
build_rpc_trait! {
|
||||||
@ -33,6 +33,10 @@ build_rpc_trait! {
|
|||||||
#[rpc(name = "signer_confirmRequest")]
|
#[rpc(name = "signer_confirmRequest")]
|
||||||
fn confirm_request(&self, U256, TransactionModification, String) -> Result<ConfirmationResponse, Error>;
|
fn confirm_request(&self, U256, TransactionModification, String) -> Result<ConfirmationResponse, Error>;
|
||||||
|
|
||||||
|
/// Confirm specific request with already signed data.
|
||||||
|
#[rpc(name = "signer_confirmRequestRaw")]
|
||||||
|
fn confirm_request_raw(&self, U256, Bytes) -> Result<ConfirmationResponse, Error>;
|
||||||
|
|
||||||
/// Reject the confirmation request.
|
/// Reject the confirmation request.
|
||||||
#[rpc(name = "signer_rejectRequest")]
|
#[rpc(name = "signer_rejectRequest")]
|
||||||
fn reject_request(&self, U256) -> Result<bool, Error>;
|
fn reject_request(&self, U256) -> Result<bool, Error>;
|
||||||
|
Loading…
Reference in New Issue
Block a user