From 2f98169539bb7645a5d21b0180bfe00784b20f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 10 Nov 2016 11:27:05 +0100 Subject: [PATCH] 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 --- ethcore/src/account_provider.rs | 12 ++- js/package.json | 2 + js/src/api/format/input.js | 4 + js/src/api/rpc/parity/parity.js | 6 ++ js/src/api/rpc/signer/signer.js | 7 +- js/src/jsonrpc/interfaces/parity.js | 14 +++ js/src/jsonrpc/interfaces/signer.js | 20 +++- js/src/redux/providers/signerMiddleware.js | 66 ++++++++++--- js/src/util/wallet.js | 82 +++++++++++++++ .../RequestPendingWeb3/RequestPendingWeb3.js | 13 ++- .../TransactionPending/TransactionPending.css | 2 +- .../TransactionPending/TransactionPending.js | 6 +- .../TransactionPendingFormConfirm.css | 4 + .../TransactionPendingFormConfirm.js | 56 +++++++++-- js/webpack.vendor.js | 1 + rpc/src/v1/helpers/errors.rs | 6 +- rpc/src/v1/impls/eth.rs | 6 +- rpc/src/v1/impls/signer.rs | 69 +++++++++++-- rpc/src/v1/tests/mocked/eth.rs | 9 +- rpc/src/v1/tests/mocked/signer.rs | 99 ++++++++++++++++++- rpc/src/v1/traits/signer.rs | 6 +- 21 files changed, 447 insertions(+), 43 deletions(-) create mode 100644 js/src/util/wallet.js diff --git a/ethcore/src/account_provider.rs b/ethcore/src/account_provider.rs index 064c3e935..e906aefe9 100644 --- a/ethcore/src/account_provider.rs +++ b/ethcore/src/account_provider.rs @@ -95,6 +95,7 @@ impl KeyDirectory for NullDir { struct AddressBook { path: PathBuf, cache: HashMap, + transient: bool, } impl AddressBook { @@ -106,11 +107,18 @@ impl AddressBook { let mut r = AddressBook { path: path, cache: HashMap::new(), + transient: false, }; r.revert(); r } + pub fn transient() -> Self { + let mut book = AddressBook::new(Default::default()); + book.transient = true; + book + } + pub fn get(&self) -> HashMap { self.cache.clone() } @@ -134,6 +142,7 @@ impl AddressBook { } fn revert(&mut self) { + if self.transient { return; } trace!(target: "addressbook", "revert"); let _ = fs::File::open(self.path.clone()) .map_err(|e| trace!(target: "addressbook", "Couldn't open address book: {}", e)) @@ -144,6 +153,7 @@ impl AddressBook { } fn save(&mut self) { + if self.transient { return; } trace!(target: "addressbook", "save"); let _ = fs::File::create(self.path.clone()) .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 { AccountProvider { 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())) .expect("NullDir load always succeeds; qed")) } diff --git a/js/package.json b/js/package.json index df9e37811..eb0926c4b 100644 --- a/js/package.json +++ b/js/package.json @@ -118,6 +118,7 @@ "bytes": "^2.4.0", "chart.js": "^2.3.0", "es6-promise": "^3.2.1", + "ethereumjs-tx": "^1.1.2", "file-saver": "^1.3.3", "format-json": "^1.0.3", "format-number": "^2.0.1", @@ -147,6 +148,7 @@ "redux-actions": "^0.10.1", "redux-thunk": "^2.1.0", "rlp": "^2.0.0", + "scryptsy": "^2.0.0", "store": "^1.3.20", "utf8": "^2.1.1", "validator": "^5.7.0", diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js index b46148cdc..830ca0e21 100644 --- a/js/src/api/format/input.js +++ b/js/src/api/format/input.js @@ -93,6 +93,10 @@ export function inFilter (options) { } export function inHex (str) { + if (str && str.toString) { + str = str.toString(16); + } + if (str && str.substr(0, 2) === '0x') { return str.toLowerCase(); } diff --git a/js/src/api/rpc/parity/parity.js b/js/src/api/rpc/parity/parity.js index 5999c9d67..a33828b80 100644 --- a/js/src/api/rpc/parity/parity.js +++ b/js/src/api/rpc/parity/parity.js @@ -181,6 +181,12 @@ export default class Parity { .then(outAddress); } + nextNonce (account) { + return this._transport + .execute('parity_nextNonce', inAddress(account)) + .then(outNumber); + } + nodeName () { return this._transport .execute('parity_nodeName'); diff --git a/js/src/api/rpc/signer/signer.js b/js/src/api/rpc/signer/signer.js index 7f905cf50..126ce651a 100644 --- a/js/src/api/rpc/signer/signer.js +++ b/js/src/api/rpc/signer/signer.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { inNumber16 } from '../../format/input'; +import { inNumber16, inData } from '../../format/input'; import { outSignerRequest } from '../../format/output'; export default class Signer { @@ -27,6 +27,11 @@ export default class Signer { .execute('signer_confirmRequest', inNumber16(requestId), options, password); } + confirmRequestRaw (requestId, data) { + return this._transport + .execute('signer_confirmRequestRaw', inNumber16(requestId), inData(data)); + } + generateAuthorizationToken () { return this._transport .execute('signer_generateAuthorizationToken'); diff --git a/js/src/jsonrpc/interfaces/parity.js b/js/src/jsonrpc/interfaces/parity.js index 883ad9675..5dd313e00 100644 --- a/js/src/jsonrpc/interfaces/parity.js +++ b/js/src/jsonrpc/interfaces/parity.js @@ -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: { desc: 'Returns node name (identity)', params: [], diff --git a/js/src/jsonrpc/interfaces/signer.js b/js/src/jsonrpc/interfaces/signer.js index f394dbb61..f50bb1115 100644 --- a/js/src/jsonrpc/interfaces/signer.js +++ b/js/src/jsonrpc/interfaces/signer.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { Quantity } from '../types'; +import { Quantity, Data } from '../types'; export default { 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: { desc: 'Rejects a request in the signer queue', params: [ diff --git a/js/src/redux/providers/signerMiddleware.js b/js/src/redux/providers/signerMiddleware.js index 6d09eeb4e..4cc877ced 100644 --- a/js/src/redux/providers/signerMiddleware.js +++ b/js/src/redux/providers/signerMiddleware.js @@ -16,6 +16,9 @@ import * as actions from './signerActions'; +import { inHex } from '../../api/format/input'; +import { Wallet } from '../../util/wallet'; + export default class SignerMiddleware { constructor (api) { this._api = api; @@ -49,23 +52,58 @@ export default class SignerMiddleware { } onConfirmStart = (store, action) => { - const { id, password } = action.payload; + const { id, password, wallet, payload } = action.payload; - this._api.signer - .confirmRequest(id, {}, password) - .then((txHash) => { - console.log('confirmRequest', id, txHash); - if (!txHash) { - store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' })); - return; + const handlePromise = promise => { + promise + .then((txHash) => { + console.log('confirmRequest', id, txHash); + if (!txHash) { + store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' })); + return; + } + + store.dispatch(actions.successConfirmRequest({ id, txHash })); + }) + .catch((error) => { + console.error('confirmRequest', id, error); + 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 })); } - - store.dispatch(actions.successConfirmRequest({ id, txHash })); - }) - .catch((error) => { - console.error('confirmRequest', id, error); - store.dispatch(actions.errorConfirmRequest({ id, err: error.message })); }); + return; + } + + handlePromise(this._api.signer.confirmRequest(id, {}, password)); } onRejectStart = (store, action) => { diff --git a/js/src/util/wallet.js b/js/src/util/wallet.js new file mode 100644 index 000000000..14c3a6016 --- /dev/null +++ b/js/src/util/wallet.js @@ -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 . + +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')); + } + +} diff --git a/js/src/views/Signer/components/RequestPendingWeb3/RequestPendingWeb3.js b/js/src/views/Signer/components/RequestPendingWeb3/RequestPendingWeb3.js index 4014f328b..97fa43f69 100644 --- a/js/src/views/Signer/components/RequestPendingWeb3/RequestPendingWeb3.js +++ b/js/src/views/Signer/components/RequestPendingWeb3/RequestPendingWeb3.js @@ -33,15 +33,22 @@ export default class RequestPendingWeb3 extends Component { className: PropTypes.string }; + onConfirm = data => { + const { onConfirm, payload } = this.props; + + data.payload = payload; + onConfirm(data); + }; + render () { - const { payload, id, className, isSending, date, onConfirm, onReject } = this.props; + const { payload, id, className, isSending, date, onReject } = this.props; if (payload.sign) { const { sign } = payload; return ( * { vertical-align: middle; - min-height: 120px; + min-height: 190px; } .inputs { diff --git a/js/src/views/Signer/components/TransactionPending/TransactionPending.js b/js/src/views/Signer/components/TransactionPending/TransactionPending.js index 77d02d0b1..55e4f6405 100644 --- a/js/src/views/Signer/components/TransactionPending/TransactionPending.js +++ b/js/src/views/Signer/components/TransactionPending/TransactionPending.js @@ -116,9 +116,11 @@ export default class TransactionPending extends Component { ); } - onConfirm = password => { + onConfirm = data => { const { id, gasPrice } = this.props; - this.props.onConfirm({ id, password, gasPrice }); + const { password, wallet } = data; + + this.props.onConfirm({ id, password, wallet, gasPrice }); } onReject = () => { diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css index 673b045d2..d10e634ae 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css @@ -40,3 +40,7 @@ .passwordHint span { opacity: 0.85; } + +.fileInput input { + top: 22px; +} diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.js index 5b54586a4..5765447ee 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.js @@ -35,26 +35,33 @@ class TransactionPendingFormConfirm extends Component { id = Math.random(); // for tooltip state = { + walletError: null, + wallet: null, password: '' } render () { const { accounts, address, isSending } = this.props; - const { password } = this.state; + const { password, walletError, wallet } = this.state; const account = accounts[address] || {}; + const isExternal = !account.uuid; const passwordHint = account.meta && account.meta.passwordHint ? (
(hint) { account.meta.passwordHint }
) : null; + const isWalletOk = !isExternal || (walletError === null && wallet !== null); + const keyInput = isExternal ? this.renderKeyInput() : null; + return (
+ { keyInput }
@@ -71,7 +78,7 @@ class TransactionPendingFormConfirm extends Component { className={ styles.confirmButton } fullWidth primary - disabled={ isSending } + disabled={ isSending || !isWalletOk } icon={ } label={ isSending ? 'Confirming...' : 'Confirm Transaction' } /> @@ -82,6 +89,20 @@ class TransactionPendingFormConfirm extends Component { ); } + renderKeyInput () { + const { walletError } = this.state; + + return ( + + ); + } + renderTooltip () { if (this.state.password.length) { 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 => { const password = evt.target.value; this.setState({ @@ -102,8 +143,11 @@ class TransactionPendingFormConfirm extends Component { } onConfirm = () => { - const { password } = this.state; - this.props.onConfirm(password); + const { password, wallet } = this.state; + + this.props.onConfirm({ + password, wallet + }); } onKeyDown = evt => { diff --git a/js/webpack.vendor.js b/js/webpack.vendor.js index a896cbfd8..74a51dada 100644 --- a/js/webpack.vendor.js +++ b/js/webpack.vendor.js @@ -22,6 +22,7 @@ const DEST = process.env.BUILD_DEST || '.build'; let modules = [ 'babel-polyfill', + 'browserify-aes', 'ethereumjs-tx', 'scryptsy', 'react', 'react-dom', 'react-redux', 'react-router', 'redux', 'redux-thunk', 'react-router-redux', 'lodash', 'material-ui', 'moment', 'blockies' diff --git a/rpc/src/v1/helpers/errors.rs b/rpc/src/v1/helpers/errors.rs index 50c22b187..d36feca4b 100644 --- a/rpc/src/v1/helpers/errors.rs +++ b/rpc/src/v1/helpers/errors.rs @@ -17,7 +17,7 @@ //! RPC Error codes and error objects macro_rules! rpc_unimplemented { - () => (Err(::v1::helpers::errors::unimplemented())) + () => (Err(::v1::helpers::errors::unimplemented(None))) } use std::fmt; @@ -51,11 +51,11 @@ mod codes { pub const FETCH_ERROR: i64 = -32060; } -pub fn unimplemented() -> Error { +pub fn unimplemented(details: Option) -> Error { Error { code: ErrorCode::ServerError(codes::UNSUPPORTED_REQUEST), message: "This request is not implemented yet. Please create an issue on Github repo.".into(), - data: None + data: details.map(Value::String), } } diff --git a/rpc/src/v1/impls/eth.rs b/rpc/src/v1/impls/eth.rs index 6986cf0ed..8207426ba 100644 --- a/rpc/src/v1/impls/eth.rs +++ b/rpc/src/v1/impls/eth.rs @@ -20,6 +20,7 @@ extern crate ethash; use std::io::{Write}; use std::process::{Command, Stdio}; +use std::collections::BTreeSet; use std::thread; use std::time::{Instant, Duration}; use std::sync::{Arc, Weak}; @@ -339,7 +340,10 @@ impl Eth for EthClient where let store = take_weak!(self.accounts); 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
= accounts.into_iter().chain(addresses.keys().cloned()).collect(); + Ok(set.into_iter().map(Into::into).collect()) } fn block_number(&self) -> Result { diff --git a/rpc/src/v1/impls/signer.rs b/rpc/src/v1/impls/signer.rs index 0ee06b5c5..66f46ba01 100644 --- a/rpc/src/v1/impls/signer.rs +++ b/rpc/src/v1/impls/signer.rs @@ -18,14 +18,17 @@ use std::sync::{Arc, Weak}; -use jsonrpc_core::*; +use rlp::{UntrustedRlp, View}; use ethcore::account_provider::AccountProvider; use ethcore::client::MiningBlockChainClient; +use ethcore::transaction::SignedTransaction; use ethcore::miner::MinerService; + +use jsonrpc_core::Error; 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::dispatch; +use v1::helpers::dispatch::{self, dispatch_transaction}; /// Transactions confirmation (personal) rpc implementation. pub struct SignerClient where C: MiningBlockChainClient, M: MinerService { @@ -66,9 +69,9 @@ impl Signer for SignerClient where C: MiningBlockC let signer = take_weak!(self.signer); Ok(signer.requests() - .into_iter() - .map(Into::into) - .collect() + .into_iter() + .map(Into::into) + .collect() ) } @@ -101,6 +104,60 @@ impl Signer for SignerClient where C: MiningBlockC }).unwrap_or_else(|| Err(errors::invalid_params("Unknown RequestID", id))) } + fn confirm_request_raw(&self, id: U256, bytes: Bytes) -> Result { + 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 { try!(self.active()); let signer = take_weak!(self.signer); diff --git a/rpc/src/v1/tests/mocked/eth.rs b/rpc/src/v1/tests/mocked/eth.rs index 7119de2c1..67e77a6db 100644 --- a/rpc/src/v1/tests/mocked/eth.rs +++ b/rpc/src/v1/tests/mocked/eth.rs @@ -353,9 +353,16 @@ fn rpc_eth_gas_price() { fn rpc_eth_accounts() { let tester = EthTester::default(); 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 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())); } diff --git a/rpc/src/v1/tests/mocked/signer.rs b/rpc/src/v1/tests/mocked/signer.rs index 8807e2373..e2ba580e0 100644 --- a/rpc/src/v1/tests/mocked/signer.rs +++ b/rpc/src/v1/tests/mocked/signer.rs @@ -16,11 +16,14 @@ use std::sync::Arc; use std::str::FromStr; -use jsonrpc_core::IoHandler; -use util::{U256, Uint, Address}; +use util::{U256, Uint, Address, ToPretty}; + use ethcore::account_provider::AccountProvider; use ethcore::client::TestBlockChainClient; use ethcore::transaction::{Transaction, Action}; +use rlp::encode; + +use jsonrpc_core::IoHandler; use v1::{SignerClient, Signer}; use v1::tests::helpers::TestMinerService; 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); } +#[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] fn should_generate_new_token() { // given diff --git a/rpc/src/v1/traits/signer.rs b/rpc/src/v1/traits/signer.rs index 26d6899cb..eafa520d4 100644 --- a/rpc/src/v1/traits/signer.rs +++ b/rpc/src/v1/traits/signer.rs @@ -18,7 +18,7 @@ use jsonrpc_core::Error; use v1::helpers::auto_args::Wrap; -use v1::types::{U256, TransactionModification, ConfirmationRequest, ConfirmationResponse}; +use v1::types::{U256, Bytes, TransactionModification, ConfirmationRequest, ConfirmationResponse}; build_rpc_trait! { @@ -33,6 +33,10 @@ build_rpc_trait! { #[rpc(name = "signer_confirmRequest")] fn confirm_request(&self, U256, TransactionModification, String) -> Result; + /// Confirm specific request with already signed data. + #[rpc(name = "signer_confirmRequestRaw")] + fn confirm_request_raw(&self, U256, Bytes) -> Result; + /// Reject the confirmation request. #[rpc(name = "signer_rejectRequest")] fn reject_request(&self, U256) -> Result;