Support external eth_sign (#5481)
* Display a QR for eth_sign requests. * Support raw confirmation of eth_sign * Fix ethkey issue on nightly. * Fixing test. * Fixing test.
This commit is contained in:
parent
43175f17e4
commit
28dcbc6426
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1711,6 +1711,7 @@ dependencies = [
|
|||||||
"order-stat 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"order-stat 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"parity-reactor 0.1.0",
|
"parity-reactor 0.1.0",
|
||||||
"parity-updater 1.7.0",
|
"parity-updater 1.7.0",
|
||||||
|
"pretty_assertions 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rlp 0.1.0",
|
"rlp 0.1.0",
|
||||||
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -57,8 +57,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_brain() {
|
fn test_brain() {
|
||||||
let words = "this is sparta!".to_owned();
|
let words = "this is sparta!".to_owned();
|
||||||
let first_keypair = Brain(words.clone()).generate().unwrap();
|
let first_keypair = Brain::new(words.clone()).generate().unwrap();
|
||||||
let second_keypair = Brain(words.clone()).generate().unwrap();
|
let second_keypair = Brain::new(words.clone()).generate().unwrap();
|
||||||
assert_eq!(first_keypair.secret(), second_keypair.secret());
|
assert_eq!(first_keypair.secret(), second_keypair.secret());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ use rustc_serialize::hex::{ToHex, FromHex};
|
|||||||
use bigint::hash::{H520, H256};
|
use bigint::hash::{H520, H256};
|
||||||
use {Secret, Public, SECP256K1, Error, Message, public_to_address, Address};
|
use {Secret, Public, SECP256K1, Error, Message, public_to_address, Address};
|
||||||
|
|
||||||
|
/// Signature encoded as RSV components
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct Signature([u8; 65]);
|
pub struct Signature([u8; 65]);
|
||||||
|
|
||||||
@ -44,8 +45,32 @@ impl Signature {
|
|||||||
self.0[64]
|
self.0[64]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode the signature into VRS array (V altered to be in "Electrum" notation).
|
||||||
|
pub fn into_vrs(self) -> [u8; 65] {
|
||||||
|
let mut vrs = [0u8; 65];
|
||||||
|
vrs[0] = self.v() + 27;
|
||||||
|
vrs[1..33].copy_from_slice(self.r());
|
||||||
|
vrs[33..65].copy_from_slice(self.s());
|
||||||
|
vrs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse bytes as a signature encoded as VRS (V in "Electrum" notation).
|
||||||
|
/// May return empty (invalid) signature if given data has invalid length.
|
||||||
|
pub fn from_vrs(data: &[u8]) -> Self {
|
||||||
|
if data.len() != 65 || data[0] < 27 {
|
||||||
|
// fallback to empty (invalid) signature
|
||||||
|
return Signature::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sig = [0u8; 65];
|
||||||
|
sig[0..32].copy_from_slice(&data[1..33]);
|
||||||
|
sig[32..64].copy_from_slice(&data[33..65]);
|
||||||
|
sig[64] = data[0] - 27;
|
||||||
|
Signature(sig)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a signature object from the sig.
|
/// Create a signature object from the sig.
|
||||||
pub fn from_rsv(r: &H256, s: &H256, v: u8) -> Signature {
|
pub fn from_rsv(r: &H256, s: &H256, v: u8) -> Self {
|
||||||
let mut sig = [0u8; 65];
|
let mut sig = [0u8; 65];
|
||||||
sig[0..32].copy_from_slice(&r);
|
sig[0..32].copy_from_slice(&r);
|
||||||
sig[32..64].copy_from_slice(&s);
|
sig[32..64].copy_from_slice(&s);
|
||||||
@ -222,6 +247,21 @@ mod tests {
|
|||||||
use {Generator, Random, Message};
|
use {Generator, Random, Message};
|
||||||
use super::{sign, verify_public, verify_address, recover, Signature};
|
use super::{sign, verify_public, verify_address, recover, Signature};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vrs_conversion() {
|
||||||
|
// given
|
||||||
|
let keypair = Random.generate().unwrap();
|
||||||
|
let message = Message::default();
|
||||||
|
let signature = sign(keypair.secret(), &message).unwrap();
|
||||||
|
|
||||||
|
// when
|
||||||
|
let vrs = signature.clone().into_vrs();
|
||||||
|
let from_vrs = Signature::from_vrs(&vrs);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(signature, from_vrs);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn signature_to_and_from_str() {
|
fn signature_to_and_from_str() {
|
||||||
let keypair = Random.generate().unwrap();
|
let keypair = Random.generate().unwrap();
|
||||||
|
@ -87,14 +87,20 @@ export default class SignerMiddleware {
|
|||||||
return this._hwstore.signLedger(transaction);
|
return this._hwstore.signLedger(transaction);
|
||||||
})
|
})
|
||||||
.then((rawTx) => {
|
.then((rawTx) => {
|
||||||
return this.confirmRawTransaction(store, id, rawTx);
|
return this.confirmRawRequest(store, id, rawTx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmRawTransaction (store, id, rawTx) {
|
confirmRawRequest (store, id, rawData) {
|
||||||
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
||||||
|
|
||||||
return handlePromise(this._api.signer.confirmRequestRaw(id, rawTx));
|
return handlePromise(this._api.signer.confirmRequestRaw(id, rawData));
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmSignedData (store, id, dataSigned) {
|
||||||
|
const { signature } = dataSigned;
|
||||||
|
|
||||||
|
return this.confirmRawRequest(store, id, signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmSignedTransaction (store, id, txSigned) {
|
confirmSignedTransaction (store, id, txSigned) {
|
||||||
@ -102,7 +108,7 @@ export default class SignerMiddleware {
|
|||||||
const { signature, tx } = txSigned;
|
const { signature, tx } = txSigned;
|
||||||
const { rlp } = createSignedTx(netVersion, signature, tx);
|
const { rlp } = createSignedTx(netVersion, signature, tx);
|
||||||
|
|
||||||
return this.confirmRawTransaction(store, id, rlp);
|
return this.confirmRawRequest(store, id, rlp);
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmWalletTransaction (store, id, transaction, wallet, password) {
|
confirmWalletTransaction (store, id, transaction, wallet, password) {
|
||||||
@ -138,7 +144,7 @@ export default class SignerMiddleware {
|
|||||||
return signer.signTransaction(txData);
|
return signer.signTransaction(txData);
|
||||||
})
|
})
|
||||||
.then((rawTx) => {
|
.then((rawTx) => {
|
||||||
return this.confirmRawTransaction(store, id, rawTx);
|
return this.confirmRawRequest(store, id, rawTx);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
@ -147,7 +153,7 @@ export default class SignerMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onConfirmStart = (store, action) => {
|
onConfirmStart = (store, action) => {
|
||||||
const { condition, gas = 0, gasPrice = 0, id, password, payload, txSigned, wallet } = action.payload;
|
const { condition, gas = 0, gasPrice = 0, id, password, payload, txSigned, dataSigned, wallet } = action.payload;
|
||||||
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
||||||
const transaction = payload.sendTransaction || payload.signTransaction;
|
const transaction = payload.sendTransaction || payload.signTransaction;
|
||||||
|
|
||||||
@ -170,6 +176,11 @@ export default class SignerMiddleware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO [ToDr] Support eth_sign for external wallet (wallet && !transction)
|
||||||
|
if (dataSigned) {
|
||||||
|
return this.confirmSignedData(store, id, dataSigned);
|
||||||
|
}
|
||||||
|
|
||||||
return handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice, condition }, password));
|
return handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice, condition }, password));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,8 +19,8 @@ import Transaction from 'ethereumjs-tx';
|
|||||||
import { inAddress, inHex, inNumber10 } from '~/api/format/input';
|
import { inAddress, inHex, inNumber10 } from '~/api/format/input';
|
||||||
import { sha3 } from '~/api/util/sha3';
|
import { sha3 } from '~/api/util/sha3';
|
||||||
|
|
||||||
export function createUnsignedTx (api, netVersion, gasStore, transaction) {
|
export function createUnsignedTx (api, netVersion, transaction) {
|
||||||
const { data, from, gas, gasPrice, to, value } = gasStore.overrideTransaction(transaction);
|
const { data, from, gas, gasPrice, to, value } = transaction;
|
||||||
|
|
||||||
return api.parity
|
return api.parity
|
||||||
.nextNonce(from)
|
.nextNonce(from)
|
||||||
@ -111,8 +111,18 @@ export function generateQr (from, tx, hash, rlp) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateTxQr (api, netVersion, gasStore, transaction) {
|
export function generateDataQr (data) {
|
||||||
return createUnsignedTx(api, netVersion, gasStore, transaction)
|
return Promise.resolve({
|
||||||
|
data,
|
||||||
|
value: JSON.stringify({
|
||||||
|
action: 'signData',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateTxQr (api, netVersion, transaction) {
|
||||||
|
return createUnsignedTx(api, netVersion, transaction)
|
||||||
.then((qr) => {
|
.then((qr) => {
|
||||||
qr.value = generateQr(transaction.from, qr.tx, qr.hash, qr.rlp);
|
qr.value = generateQr(transaction.from, qr.tx, qr.hash, qr.rlp);
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ export default class RequestOrigin extends Component {
|
|||||||
<span>
|
<span>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='signer.requestOrigin.rpc'
|
id='signer.requestOrigin.rpc'
|
||||||
defaultMessage='via RPC {rpc}'
|
defaultMessage='via RPC {url}'
|
||||||
values={ {
|
values={ {
|
||||||
url: (
|
url: (
|
||||||
<span className={ styles.url }>
|
<span className={ styles.url }>
|
||||||
|
@ -19,6 +19,8 @@ import React, { Component, PropTypes } from 'react';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import HardwareStore from '~/mobx/hardwareStore';
|
||||||
|
|
||||||
import Account from '../Account';
|
import Account from '../Account';
|
||||||
import TransactionPendingForm from '../TransactionPendingForm';
|
import TransactionPendingForm from '../TransactionPendingForm';
|
||||||
import RequestOrigin from '../RequestOrigin';
|
import RequestOrigin from '../RequestOrigin';
|
||||||
@ -68,6 +70,8 @@ class SignRequest extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hardwareStore = HardwareStore.get(this.context.api);
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
const { address, signerStore } = this.props;
|
const { address, signerStore } = this.props;
|
||||||
|
|
||||||
@ -155,8 +159,9 @@ class SignRequest extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderActions () {
|
renderActions () {
|
||||||
const { accounts, address, focus, isFinished, status } = this.props;
|
const { accounts, address, focus, isFinished, status, data } = this.props;
|
||||||
const account = accounts[address];
|
const account = accounts[address] || {};
|
||||||
|
const disabled = account.hardware && !this.hardwareStore.isConnected(address);
|
||||||
|
|
||||||
if (isFinished) {
|
if (isFinished) {
|
||||||
if (status === 'confirmed') {
|
if (status === 'confirmed') {
|
||||||
@ -188,21 +193,23 @@ class SignRequest extends Component {
|
|||||||
<TransactionPendingForm
|
<TransactionPendingForm
|
||||||
account={ account }
|
account={ account }
|
||||||
address={ address }
|
address={ address }
|
||||||
|
disabled={ disabled }
|
||||||
focus={ focus }
|
focus={ focus }
|
||||||
isSending={ this.props.isSending }
|
isSending={ this.props.isSending }
|
||||||
netVersion={ this.props.netVersion }
|
netVersion={ this.props.netVersion }
|
||||||
onConfirm={ this.onConfirm }
|
onConfirm={ this.onConfirm }
|
||||||
onReject={ this.onReject }
|
onReject={ this.onReject }
|
||||||
className={ styles.actions }
|
className={ styles.actions }
|
||||||
|
dataToSign={ { data } }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfirm = (data) => {
|
onConfirm = (data) => {
|
||||||
const { id } = this.props;
|
const { id } = this.props;
|
||||||
const { password } = data;
|
const { password, dataSigned, wallet } = data;
|
||||||
|
|
||||||
this.props.onConfirm({ id, password });
|
this.props.onConfirm({ id, password, dataSigned, wallet });
|
||||||
}
|
}
|
||||||
|
|
||||||
onReject = () => {
|
onReject = () => {
|
||||||
|
@ -98,7 +98,9 @@ class TransactionPending extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTransaction () {
|
renderTransaction () {
|
||||||
const { accounts, className, focus, id, isSending, netVersion, origin, signerStore, transaction } = this.props;
|
const transaction = this.gasStore.overrideTransaction(this.props.transaction);
|
||||||
|
|
||||||
|
const { accounts, className, focus, id, isSending, netVersion, origin, signerStore } = this.props;
|
||||||
const { totalValue } = this.state;
|
const { totalValue } = this.state;
|
||||||
const { balances, externalLink } = signerStore;
|
const { balances, externalLink } = signerStore;
|
||||||
const { from, value } = transaction;
|
const { from, value } = transaction;
|
||||||
@ -127,12 +129,11 @@ class TransactionPending extends Component {
|
|||||||
address={ from }
|
address={ from }
|
||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
focus={ focus }
|
focus={ focus }
|
||||||
gasStore={ this.gasStore }
|
|
||||||
isSending={ isSending }
|
isSending={ isSending }
|
||||||
netVersion={ netVersion }
|
netVersion={ netVersion }
|
||||||
onConfirm={ this.onConfirm }
|
onConfirm={ this.onConfirm }
|
||||||
onReject={ this.onReject }
|
onReject={ this.onReject }
|
||||||
transaction={ transaction }
|
dataToSign={ { transaction } }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -22,7 +22,7 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
import ReactTooltip from 'react-tooltip';
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
|
||||||
import { Form, Input, IdentityIcon, QrCode, QrScan } from '~/ui';
|
import { Form, Input, IdentityIcon, QrCode, QrScan } from '~/ui';
|
||||||
import { generateTxQr } from '~/util/qrscan';
|
import { generateTxQr, generateDataQr } from '~/util/qrscan';
|
||||||
|
|
||||||
import styles from './transactionPendingFormConfirm.css';
|
import styles from './transactionPendingFormConfirm.css';
|
||||||
|
|
||||||
@ -40,11 +40,10 @@ export default class TransactionPendingFormConfirm extends Component {
|
|||||||
address: PropTypes.string.isRequired,
|
address: PropTypes.string.isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
focus: PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
gasStore: PropTypes.object.isRequired,
|
|
||||||
netVersion: PropTypes.string.isRequired,
|
netVersion: PropTypes.string.isRequired,
|
||||||
isSending: PropTypes.bool.isRequired,
|
isSending: PropTypes.bool.isRequired,
|
||||||
onConfirm: PropTypes.func.isRequired,
|
onConfirm: PropTypes.func.isRequired,
|
||||||
transaction: PropTypes.object.isRequired
|
dataToSign: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -406,7 +405,7 @@ export default class TransactionPendingFormConfirm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onScanTx = (signature) => {
|
onScanTx = (signature) => {
|
||||||
const { chainId, rlp, tx } = this.state.qr;
|
const { chainId, rlp, tx, data } = this.state.qr;
|
||||||
|
|
||||||
if (signature && signature.substr(0, 2) !== '0x') {
|
if (signature && signature.substr(0, 2) !== '0x') {
|
||||||
signature = `0x${signature}`;
|
signature = `0x${signature}`;
|
||||||
@ -414,6 +413,7 @@ export default class TransactionPendingFormConfirm extends Component {
|
|||||||
|
|
||||||
this.setState({ qrState: QR_COMPLETED });
|
this.setState({ qrState: QR_COMPLETED });
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
this.props.onConfirm({
|
this.props.onConfirm({
|
||||||
txSigned: {
|
txSigned: {
|
||||||
chainId,
|
chainId,
|
||||||
@ -422,6 +422,15 @@ export default class TransactionPendingFormConfirm extends Component {
|
|||||||
tx
|
tx
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onConfirm({
|
||||||
|
dataSigned: {
|
||||||
|
data,
|
||||||
|
signature
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeySelect = (event) => {
|
onKeySelect = (event) => {
|
||||||
@ -487,13 +496,20 @@ export default class TransactionPendingFormConfirm extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
generateTxQr = () => {
|
generateQr = () => {
|
||||||
const { api } = this.context;
|
const { api } = this.context;
|
||||||
const { netVersion, gasStore, transaction } = this.props;
|
const { netVersion, dataToSign } = this.props;
|
||||||
|
const { transaction, data } = dataToSign;
|
||||||
generateTxQr(api, netVersion, gasStore, transaction).then((qr) => {
|
const setState = qr => {
|
||||||
this.setState({ qr });
|
this.setState({ qr });
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (transaction) {
|
||||||
|
generateTxQr(api, netVersion, transaction).then(setState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDataQr(data).then(setState);
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown = (event) => {
|
onKeyDown = (event) => {
|
||||||
@ -528,19 +544,25 @@ export default class TransactionPendingFormConfirm extends Component {
|
|||||||
|
|
||||||
readNonce = () => {
|
readNonce = () => {
|
||||||
const { api } = this.context;
|
const { api } = this.context;
|
||||||
const { account } = this.props;
|
const { account, dataToSign } = this.props;
|
||||||
|
const { qr } = this.state;
|
||||||
|
|
||||||
if (!account || !account.external || !api.transport.isConnected) {
|
if (dataToSign.data && qr && !qr.value) {
|
||||||
|
this.generateQr();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!account || !account.external || !api.transport.isConnected || !dataToSign.transaction) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.parity
|
return api.parity
|
||||||
.nextNonce(account.address)
|
.nextNonce(account.address)
|
||||||
.then((nonce) => {
|
.then((newNonce) => {
|
||||||
const { qr } = this.state;
|
const { nonce } = this.state.qr;
|
||||||
|
|
||||||
if (!qr.nonce || !nonce.eq(qr.nonce)) {
|
if (!nonce || !newNonce.eq(nonce)) {
|
||||||
this.generateTxQr();
|
this.generateQr();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ function render (address) {
|
|||||||
address={ address }
|
address={ address }
|
||||||
onConfirm={ onConfirm }
|
onConfirm={ onConfirm }
|
||||||
isSending={ false }
|
isSending={ false }
|
||||||
|
dataToSign={ {} }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
instance = component.instance();
|
instance = component.instance();
|
||||||
|
@ -30,12 +30,18 @@ export default class TransactionPendingForm extends Component {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
focus: PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
gasStore: PropTypes.object.isRequired,
|
|
||||||
netVersion: PropTypes.string.isRequired,
|
netVersion: PropTypes.string.isRequired,
|
||||||
isSending: PropTypes.bool.isRequired,
|
isSending: PropTypes.bool.isRequired,
|
||||||
onConfirm: PropTypes.func.isRequired,
|
onConfirm: PropTypes.func.isRequired,
|
||||||
onReject: PropTypes.func.isRequired,
|
onReject: PropTypes.func.isRequired,
|
||||||
|
dataToSign: PropTypes.oneOfType([
|
||||||
|
PropTypes.shape({
|
||||||
transaction: PropTypes.object.isRequired
|
transaction: PropTypes.object.isRequired
|
||||||
|
}),
|
||||||
|
PropTypes.shape({
|
||||||
|
data: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
]).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -59,7 +65,7 @@ export default class TransactionPendingForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderForm () {
|
renderForm () {
|
||||||
const { account, address, disabled, focus, gasStore, isSending, netVersion, onConfirm, onReject, transaction } = this.props;
|
const { account, address, disabled, focus, isSending, netVersion, onConfirm, onReject, dataToSign } = this.props;
|
||||||
|
|
||||||
if (this.state.isRejectOpen) {
|
if (this.state.isRejectOpen) {
|
||||||
return (
|
return (
|
||||||
@ -73,11 +79,10 @@ export default class TransactionPendingForm extends Component {
|
|||||||
account={ account }
|
account={ account }
|
||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
focus={ focus }
|
focus={ focus }
|
||||||
gasStore={ gasStore }
|
|
||||||
netVersion={ netVersion }
|
netVersion={ netVersion }
|
||||||
isSending={ isSending }
|
isSending={ isSending }
|
||||||
onConfirm={ onConfirm }
|
onConfirm={ onConfirm }
|
||||||
transaction={ transaction }
|
dataToSign={ dataToSign }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ fetch = { path = "../util/fetch" }
|
|||||||
stats = { path = "../util/stats" }
|
stats = { path = "../util/stats" }
|
||||||
|
|
||||||
clippy = { version = "0.0.103", optional = true}
|
clippy = { version = "0.0.103", optional = true}
|
||||||
|
pretty_assertions = "0.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
dev = ["clippy", "ethcore/dev", "ethcore-util/dev", "ethsync/dev"]
|
dev = ["clippy", "ethcore/dev", "ethcore-util/dev", "ethsync/dev"]
|
||||||
|
@ -66,6 +66,10 @@ extern crate ethjson;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
extern crate ethcore_devtools as devtools;
|
extern crate ethcore_devtools as devtools;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate pretty_assertions;
|
||||||
|
|
||||||
pub extern crate jsonrpc_ws_server as ws;
|
pub extern crate jsonrpc_ws_server as ws;
|
||||||
|
|
||||||
mod metadata;
|
mod metadata;
|
||||||
|
@ -206,6 +206,17 @@ pub fn fetch_gas_price_corpus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a eth_sign-compatible hash of data to sign.
|
||||||
|
/// The data is prepended with special message to prevent
|
||||||
|
/// chosen-plaintext attacks.
|
||||||
|
pub fn eth_data_hash(mut data: Bytes) -> H256 {
|
||||||
|
let mut message_data =
|
||||||
|
format!("\x19Ethereum Signed Message:\n{}", data.len())
|
||||||
|
.into_bytes();
|
||||||
|
message_data.append(&mut data);
|
||||||
|
message_data.sha3()
|
||||||
|
}
|
||||||
|
|
||||||
/// Dispatcher for light clients -- fetches default gas price, next nonce, etc. from network.
|
/// Dispatcher for light clients -- fetches default gas price, next nonce, etc. from network.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LightDispatcher {
|
pub struct LightDispatcher {
|
||||||
@ -474,21 +485,11 @@ pub fn execute<D: Dispatcher + 'static>(
|
|||||||
.map(ConfirmationResponse::SignTransaction)
|
.map(ConfirmationResponse::SignTransaction)
|
||||||
).boxed()
|
).boxed()
|
||||||
},
|
},
|
||||||
ConfirmationPayload::EthSignMessage(address, mut data) => {
|
ConfirmationPayload::EthSignMessage(address, data) => {
|
||||||
let mut message_data =
|
let hash = eth_data_hash(data);
|
||||||
format!("\x19Ethereum Signed Message:\n{}", data.len())
|
let res = signature(&accounts, address, hash, pass)
|
||||||
.into_bytes();
|
|
||||||
message_data.append(&mut data);
|
|
||||||
let res = signature(&accounts, address, message_data.sha3(), pass)
|
|
||||||
.map(|result| result
|
.map(|result| result
|
||||||
.map(|rsv| {
|
.map(|rsv| H520(rsv.into_vrs()))
|
||||||
let mut vrs = [0u8; 65];
|
|
||||||
let rsv = rsv.as_ref();
|
|
||||||
vrs[0] = rsv[64] + 27;
|
|
||||||
vrs[1..33].copy_from_slice(&rsv[0..32]);
|
|
||||||
vrs[33..65].copy_from_slice(&rsv[32..64]);
|
|
||||||
H520(vrs)
|
|
||||||
})
|
|
||||||
.map(RpcH520::from)
|
.map(RpcH520::from)
|
||||||
.map(ConfirmationResponse::Signature)
|
.map(ConfirmationResponse::Signature)
|
||||||
);
|
);
|
||||||
|
@ -21,12 +21,13 @@ use std::sync::{Arc, Weak};
|
|||||||
use rlp::UntrustedRlp;
|
use rlp::UntrustedRlp;
|
||||||
use ethcore::account_provider::AccountProvider;
|
use ethcore::account_provider::AccountProvider;
|
||||||
use ethcore::transaction::{SignedTransaction, PendingTransaction};
|
use ethcore::transaction::{SignedTransaction, PendingTransaction};
|
||||||
|
use ethkey;
|
||||||
use futures::{future, BoxFuture, Future, IntoFuture};
|
use futures::{future, BoxFuture, Future, IntoFuture};
|
||||||
|
|
||||||
use jsonrpc_core::Error;
|
use jsonrpc_core::Error;
|
||||||
use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload};
|
|
||||||
use v1::helpers::dispatch::{self, Dispatcher, WithToken};
|
|
||||||
use v1::helpers::accounts::unwrap_provider;
|
use v1::helpers::accounts::unwrap_provider;
|
||||||
|
use v1::helpers::dispatch::{self, Dispatcher, WithToken, eth_data_hash};
|
||||||
|
use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload, FilledTransactionRequest};
|
||||||
use v1::traits::Signer;
|
use v1::traits::Signer;
|
||||||
use v1::types::{TransactionModification, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken, U256, Bytes};
|
use v1::types::{TransactionModification, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken, U256, Bytes};
|
||||||
|
|
||||||
@ -104,6 +105,37 @@ impl<D: Dispatcher + 'static> SignerClient<D> {
|
|||||||
})
|
})
|
||||||
.unwrap_or_else(|| future::err(errors::invalid_params("Unknown RequestID", id)).boxed())
|
.unwrap_or_else(|| future::err(errors::invalid_params("Unknown RequestID", id)).boxed())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn verify_transaction<F>(bytes: Bytes, request: FilledTransactionRequest, process: F) -> Result<ConfirmationResponse, Error> where
|
||||||
|
F: FnOnce(PendingTransaction) -> Result<ConfirmationResponse, Error>,
|
||||||
|
{
|
||||||
|
let signed_transaction = UntrustedRlp::new(&bytes.0).as_val().map_err(errors::from_rlp_error)?;
|
||||||
|
let signed_transaction = SignedTransaction::new(signed_transaction).map_err(|e| errors::invalid_params("Invalid signature.", e))?;
|
||||||
|
let sender = signed_transaction.sender();
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
let pending_transaction = PendingTransaction::new(signed_transaction, request.condition.map(Into::into));
|
||||||
|
process(pending_transaction)
|
||||||
|
} 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Dispatcher + 'static> Signer for SignerClient<D> {
|
impl<D: Dispatcher + 'static> Signer for SignerClient<D> {
|
||||||
@ -149,38 +181,27 @@ impl<D: Dispatcher + 'static> Signer for SignerClient<D> {
|
|||||||
signer.peek(&id).map(|confirmation| {
|
signer.peek(&id).map(|confirmation| {
|
||||||
let result = match confirmation.payload {
|
let result = match confirmation.payload {
|
||||||
ConfirmationPayload::SendTransaction(request) => {
|
ConfirmationPayload::SendTransaction(request) => {
|
||||||
let signed_transaction = UntrustedRlp::new(&bytes.0).as_val().map_err(errors::from_rlp_error)?;
|
Self::verify_transaction(bytes, request, |pending_transaction| {
|
||||||
let signed_transaction = SignedTransaction::new(signed_transaction).map_err(|e| errors::invalid_params("Invalid signature.", e))?;
|
|
||||||
let sender = signed_transaction.sender();
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
let pending_transaction = PendingTransaction::new(signed_transaction, request.condition.map(Into::into));
|
|
||||||
self.dispatcher.dispatch_transaction(pending_transaction)
|
self.dispatcher.dispatch_transaction(pending_transaction)
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.map(ConfirmationResponse::SendTransaction)
|
.map(ConfirmationResponse::SendTransaction)
|
||||||
} else {
|
})
|
||||||
let mut error = Vec::new();
|
},
|
||||||
if !sender_matches { error.push("from") }
|
ConfirmationPayload::SignTransaction(request) => {
|
||||||
if !data_matches { error.push("data") }
|
Self::verify_transaction(bytes, request, |pending_transaction| {
|
||||||
if !value_matches { error.push("value") }
|
Ok(ConfirmationResponse::SignTransaction(pending_transaction.transaction.into()))
|
||||||
if !nonce_matches { error.push("nonce") }
|
})
|
||||||
|
},
|
||||||
Err(errors::invalid_params("Sent transaction does not match the request.", error))
|
ConfirmationPayload::EthSignMessage(address, data) => {
|
||||||
|
let expected_hash = eth_data_hash(data);
|
||||||
|
let signature = ethkey::Signature::from_vrs(&bytes.0);
|
||||||
|
match ethkey::verify_address(&address, &signature, &expected_hash) {
|
||||||
|
Ok(true) => Ok(ConfirmationResponse::Signature(bytes.0.as_slice().into())),
|
||||||
|
Ok(false) => Err(errors::invalid_params("Sender address does not match the signature.", ())),
|
||||||
|
Err(err) => Err(errors::invalid_params("Invalid signature received.", err)),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// TODO [ToDr]:
|
// TODO [ToDr]: Decrypt - pass through?
|
||||||
// 1. Sign - verify signature
|
|
||||||
// 2. Decrypt - pass through?
|
|
||||||
_ => Err(errors::unimplemented(Some("Non-transaction requests does not support RAW signing yet.".into()))),
|
_ => Err(errors::unimplemented(Some("Non-transaction requests does not support RAW signing yet.".into()))),
|
||||||
};
|
};
|
||||||
if let Ok(ref response) = result {
|
if let Ok(ref response) = result {
|
||||||
|
@ -20,7 +20,7 @@ use util::{U256, Uint, Address, ToPretty};
|
|||||||
|
|
||||||
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, SignedTransaction};
|
||||||
use rlp::encode;
|
use rlp::encode;
|
||||||
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
@ -28,8 +28,9 @@ use jsonrpc_core::IoHandler;
|
|||||||
use v1::{SignerClient, Signer, Origin};
|
use v1::{SignerClient, Signer, Origin};
|
||||||
use v1::metadata::Metadata;
|
use v1::metadata::Metadata;
|
||||||
use v1::tests::helpers::TestMinerService;
|
use v1::tests::helpers::TestMinerService;
|
||||||
|
use v1::types::H520;
|
||||||
use v1::helpers::{SigningQueue, SignerService, FilledTransactionRequest, ConfirmationPayload};
|
use v1::helpers::{SigningQueue, SignerService, FilledTransactionRequest, ConfirmationPayload};
|
||||||
use v1::helpers::dispatch::FullDispatcher;
|
use v1::helpers::dispatch::{FullDispatcher, eth_data_hash};
|
||||||
|
|
||||||
struct SignerTester {
|
struct SignerTester {
|
||||||
signer: Arc<SignerService>,
|
signer: Arc<SignerService>,
|
||||||
@ -359,7 +360,6 @@ fn should_confirm_transaction_with_rlp() {
|
|||||||
"params":["0x1", "0x"#.to_owned() + &rlp.to_hex() + r#""],
|
"params":["0x1", "0x"#.to_owned() + &rlp.to_hex() + r#""],
|
||||||
"id":1
|
"id":1
|
||||||
}"#;
|
}"#;
|
||||||
println!("{:?}", request);
|
|
||||||
let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#;
|
let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#;
|
||||||
|
|
||||||
// then
|
// then
|
||||||
@ -415,6 +415,101 @@ fn should_return_error_when_sender_does_not_match() {
|
|||||||
assert_eq!(tester.signer.requests().len(), 1);
|
assert_eq!(tester.signer.requests().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_confirm_sign_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::SignTransaction(FilledTransactionRequest {
|
||||||
|
from: address,
|
||||||
|
used_default_from: false,
|
||||||
|
to: Some(recipient),
|
||||||
|
gas_price: U256::from(10_000),
|
||||||
|
gas: U256::from(10_000_000),
|
||||||
|
value: U256::from(1),
|
||||||
|
data: vec![],
|
||||||
|
nonce: None,
|
||||||
|
condition: None,
|
||||||
|
}), Origin::Unknown).unwrap();
|
||||||
|
assert_eq!(tester.signer.requests().len(), 1);
|
||||||
|
|
||||||
|
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![]
|
||||||
|
};
|
||||||
|
let signature = tester.accounts.sign(address, Some("test".into()), t.hash(None)).unwrap();
|
||||||
|
let t = SignedTransaction::new(t.with_signature(signature.clone(), None)).unwrap();
|
||||||
|
let rlp = encode(&t);
|
||||||
|
|
||||||
|
// 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","result":{"#.to_owned() +
|
||||||
|
r#""raw":"0x"# + &rlp.to_hex() + r#"","# +
|
||||||
|
r#""tx":{"# +
|
||||||
|
r#""blockHash":null,"blockNumber":null,"condition":null,"creates":null,"# +
|
||||||
|
&format!("\"from\":\"0x{:?}\",", &address) +
|
||||||
|
r#""gas":"0x989680","gasPrice":"0x1000","# +
|
||||||
|
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
|
||||||
|
r#""input":"0x","# +
|
||||||
|
&format!("\"networkId\":{},", t.network_id().map_or("null".to_owned(), |n| format!("{}", n))) +
|
||||||
|
r#""nonce":"0x0","# +
|
||||||
|
&format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) +
|
||||||
|
&format!("\"r\":\"0x{}\",", U256::from(signature.r()).to_hex()) +
|
||||||
|
&format!("\"raw\":\"0x{}\",", rlp.to_hex()) +
|
||||||
|
&format!("\"s\":\"0x{}\",", U256::from(signature.s()).to_hex()) +
|
||||||
|
&format!("\"standardV\":\"0x{}\",", U256::from(t.standard_v()).to_hex()) +
|
||||||
|
r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# +
|
||||||
|
&format!("\"v\":\"0x{}\",", U256::from(t.original_v()).to_hex()) +
|
||||||
|
r#""value":"0x1""# +
|
||||||
|
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(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_confirm_data_sign_with_signature() {
|
||||||
|
// given
|
||||||
|
let tester = signer_tester();
|
||||||
|
let address = tester.accounts.new_account("test").unwrap();
|
||||||
|
tester.signer.add_request(ConfirmationPayload::EthSignMessage(
|
||||||
|
address,
|
||||||
|
vec![1, 2, 3, 4].into(),
|
||||||
|
), Origin::Unknown).unwrap();
|
||||||
|
assert_eq!(tester.signer.requests().len(), 1);
|
||||||
|
|
||||||
|
let data_hash = eth_data_hash(vec![1, 2, 3, 4].into());
|
||||||
|
let signature = H520(tester.accounts.sign(address, Some("test".into()), data_hash).unwrap().into_vrs());
|
||||||
|
let signature = format!("0x{:?}", signature);
|
||||||
|
|
||||||
|
// when
|
||||||
|
let request = r#"{
|
||||||
|
"jsonrpc":"2.0",
|
||||||
|
"method":"signer_confirmRequestRaw",
|
||||||
|
"params":["0x1", ""#.to_owned() + &signature + r#""],
|
||||||
|
"id":1
|
||||||
|
}"#;
|
||||||
|
let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + &signature + 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(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_generate_new_token() {
|
fn should_generate_new_token() {
|
||||||
// given
|
// given
|
||||||
|
@ -129,7 +129,7 @@ pub enum ConfirmationResponse {
|
|||||||
SendTransaction(H256),
|
SendTransaction(H256),
|
||||||
/// Transaction RLP
|
/// Transaction RLP
|
||||||
SignTransaction(RichRawTransaction),
|
SignTransaction(RichRawTransaction),
|
||||||
/// Signature
|
/// Signature (encoded as VRS)
|
||||||
Signature(H520),
|
Signature(H520),
|
||||||
/// Decrypted data
|
/// Decrypted data
|
||||||
Decrypt(Bytes),
|
Decrypt(Bytes),
|
||||||
|
Loading…
Reference in New Issue
Block a user