Enable Parity Signer Message Signing (#1663)

* Enable Parity Signer to sign messages

* Verify that message signature is correct

* Type systems are awesome :)
This commit is contained in:
Maciej Hirsz 2018-04-26 02:36:29 +02:00 committed by Daniel Ternyak
parent 5542791af8
commit cf59688896
24 changed files with 329 additions and 72 deletions

View File

@ -0,0 +1,28 @@
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
import { ISignedMessage } from 'libs/signing';
export type TSignMessageRequested = typeof signMessageRequested;
export function signMessageRequested(payload: string): interfaces.SignMessageRequestedAction {
return {
type: TypeKeys.SIGN_MESSAGE_REQUESTED,
payload
};
}
export type TSignLocalMessageSucceeded = typeof signLocalMessageSucceeded;
export function signLocalMessageSucceeded(
payload: ISignedMessage
): interfaces.SignLocalMessageSucceededAction {
return {
type: TypeKeys.SIGN_LOCAL_MESSAGE_SUCCEEDED,
payload
};
}
export type TSignMessageFailed = typeof signMessageFailed;
export function signMessageFailed(): interfaces.SignMessageFailedAction {
return {
type: TypeKeys.SIGN_MESSAGE_FAILED
};
}

View File

@ -0,0 +1,22 @@
import { TypeKeys } from './constants';
import { ISignedMessage } from 'libs/signing';
export interface SignMessageRequestedAction {
type: TypeKeys.SIGN_MESSAGE_REQUESTED;
payload: string;
}
export interface SignLocalMessageSucceededAction {
type: TypeKeys.SIGN_LOCAL_MESSAGE_SUCCEEDED;
payload: ISignedMessage;
}
export interface SignMessageFailedAction {
type: TypeKeys.SIGN_MESSAGE_FAILED;
}
/*** Union Type ***/
export type MessageAction =
| SignMessageRequestedAction
| SignLocalMessageSucceededAction
| SignMessageFailedAction;

View File

@ -0,0 +1,5 @@
export enum TypeKeys {
SIGN_MESSAGE_REQUESTED = 'SIGN_MESSAGE_REQUESTED',
SIGN_LOCAL_MESSAGE_SUCCEEDED = 'SIGN_LOCAL_MESSAGE_SUCCEEDED',
SIGN_MESSAGE_FAILED = 'SIGN_MESSAGE_FAILED'
}

View File

@ -0,0 +1,3 @@
export * from './actionCreators';
export * from './constants';
export * from './actionTypes';

View File

@ -1,13 +1,32 @@
import * as types from './actionTypes'; import * as types from './actionTypes';
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
export type TRequestSignature = typeof requestSignature; export type TRequestTransactionSignature = typeof requestTransactionSignature;
export function requestSignature(from: string, rlp: string): types.RequestSignatureAction { export function requestTransactionSignature(
from: string,
data: string
): types.RequestTransactionSignatureAction {
return { return {
type: TypeKeys.PARITY_SIGNER_REQUEST_SIGNATURE, type: TypeKeys.PARITY_SIGNER_REQUEST_TX_SIGNATURE,
payload: { payload: {
isMessage: false,
from, from,
rlp data
}
};
}
export type TRequestMessageSignature = typeof requestMessageSignature;
export function requestMessageSignature(
from: string,
data: string
): types.RequestMessageSignatureAction {
return {
type: TypeKeys.PARITY_SIGNER_REQUEST_MSG_SIGNATURE,
payload: {
isMessage: true,
from,
data
} }
}; };
} }

View File

@ -1,9 +1,19 @@
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
export interface RequestSignatureAction { export interface RequestTransactionSignatureAction {
type: TypeKeys.PARITY_SIGNER_REQUEST_SIGNATURE; type: TypeKeys.PARITY_SIGNER_REQUEST_TX_SIGNATURE;
payload: { payload: {
rlp: string; isMessage: false;
data: string;
from: string;
};
}
export interface RequestMessageSignatureAction {
type: TypeKeys.PARITY_SIGNER_REQUEST_MSG_SIGNATURE;
payload: {
isMessage: true;
data: string;
from: string; from: string;
}; };
} }
@ -14,4 +24,7 @@ export interface FinalizeSignatureAction {
} }
/*** Union Type ***/ /*** Union Type ***/
export type ParitySignerAction = RequestSignatureAction | FinalizeSignatureAction; export type ParitySignerAction =
| RequestTransactionSignatureAction
| RequestMessageSignatureAction
| FinalizeSignatureAction;

View File

@ -1,4 +1,5 @@
export enum TypeKeys { export enum TypeKeys {
PARITY_SIGNER_REQUEST_SIGNATURE = 'PARITY_SIGNER_REQUEST_SIGNATURE', PARITY_SIGNER_REQUEST_TX_SIGNATURE = 'PARITY_SIGNER_REQUEST_TX_SIGNATURE',
PARITY_SIGNER_REQUEST_MSG_SIGNATURE = 'PARITY_SIGNER_REQUEST_MSG_SIGNATURE',
PARITY_SIGNER_FINALIZE_SIGNATURE = 'PARITY_SIGNER_FINALIZE_SIGNATURE' PARITY_SIGNER_FINALIZE_SIGNATURE = 'PARITY_SIGNER_FINALIZE_SIGNATURE'
} }

View File

@ -18,7 +18,8 @@ interface ScanProps {
interface ShowProps { interface ShowProps {
scan: false; scan: false;
account: string; account: string;
rlp: string; data?: string;
rlp?: string;
} }
interface SharedProps { interface SharedProps {

View File

@ -22,10 +22,9 @@ export const DISABLE_WALLETS: { [key in WalletMode]: DisabledWallets } = {
} }
}, },
[WalletMode.UNABLE_TO_SIGN]: { [WalletMode.UNABLE_TO_SIGN]: {
wallets: [SecureWalletName.TREZOR, SecureWalletName.PARITY_SIGNER, MiscWalletName.VIEW_ONLY], wallets: [SecureWalletName.TREZOR, MiscWalletName.VIEW_ONLY],
reasons: { reasons: {
[SecureWalletName.TREZOR]: 'This wallet cant sign messages', [SecureWalletName.TREZOR]: 'This wallet cant sign messages',
[SecureWalletName.PARITY_SIGNER]: 'This wallet cant sign messages',
[MiscWalletName.VIEW_ONLY]: 'This wallet cant sign messages' [MiscWalletName.VIEW_ONLY]: 'This wallet cant sign messages'
} }
} }

View File

@ -16,8 +16,9 @@ interface PropsClosed {
interface PropsOpen { interface PropsOpen {
isOpen: true; isOpen: true;
rlp: string; isMessage: boolean;
from: string; from: string;
data: string;
} }
interface ActionProps { interface ActionProps {
@ -40,7 +41,7 @@ class QrSignerModal extends React.Component<Props, State> {
} }
const { scan } = this.state; const { scan } = this.state;
const { from, rlp } = this.props; const { from, data, isMessage } = this.props;
const buttons: IButton[] = [ const buttons: IButton[] = [
{ {
@ -60,7 +61,7 @@ class QrSignerModal extends React.Component<Props, State> {
return ( return (
<div className="QrSignerModal"> <div className="QrSignerModal">
<Modal <Modal
title={translateRaw('DEP_SIGNTX')} title={translateRaw(isMessage ? 'NAV_SIGNMSG' : 'DEP_SIGNTX')}
isOpen={true} isOpen={true}
buttons={buttons} buttons={buttons}
handleClose={this.onClose} handleClose={this.onClose}
@ -68,8 +69,10 @@ class QrSignerModal extends React.Component<Props, State> {
<div className="QrSignerModal-qr-bounds"> <div className="QrSignerModal-qr-bounds">
{scan ? ( {scan ? (
<ParityQrSigner scan={true} onScan={this.onScan} /> <ParityQrSigner scan={true} onScan={this.onScan} />
) : isMessage ? (
<ParityQrSigner scan={false} account={from} data={data} />
) : ( ) : (
<ParityQrSigner scan={false} account={from} rlp={rlp} /> <ParityQrSigner scan={false} account={from} rlp={data} />
)} )}
</div> </div>
</Modal> </Modal>
@ -103,12 +106,9 @@ function mapStateToProps(state: AppState): PropsClosed | PropsOpen {
return { isOpen: false }; return { isOpen: false };
} }
const { from, rlp } = requested;
return { return {
isOpen: true, isOpen: true,
from, ...requested
rlp
}; };
} }

View File

@ -1,14 +1,10 @@
import React from 'react'; import React from 'react';
import translate from 'translations'; import translate from 'translations';
import { ISignedMessage } from 'libs/signing'; import { TSignMessageRequested } from 'actions/message';
import { IFullWallet } from 'libs/wallet';
import { TShowNotification } from 'actions/notifications';
interface Props { interface Props {
wallet: IFullWallet;
message: string; message: string;
showNotification: TShowNotification; signMessageRequested: TSignMessageRequested;
onSignMessage(msg: ISignedMessage): any;
} }
export default class SignMessageButton extends React.Component<Props, {}> { export default class SignMessageButton extends React.Component<Props, {}> {
@ -20,24 +16,9 @@ export default class SignMessageButton extends React.Component<Props, {}> {
); );
} }
private handleSignMessage = async () => { private handleSignMessage = () => {
const { wallet, message, showNotification, onSignMessage } = this.props; const { signMessageRequested, message } = this.props;
try { signMessageRequested(message);
const signedMessage: ISignedMessage = {
address: wallet.getAddressString(),
msg: message,
sig: await wallet.signMessage(message),
version: '2'
};
onSignMessage(signedMessage);
showNotification(
'success',
translate('SIGN_MSG_SUCCESS', { $address: signedMessage.address })
);
} catch (err) {
showNotification('danger', translate('SIGN_MSG_FAIL', { $err: err.message }));
}
}; };
} }

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt'; import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications'; import { signMessageRequested, TSignMessageRequested } from 'actions/message';
import { resetWallet, TResetWallet } from 'actions/wallet'; import { resetWallet, TResetWallet } from 'actions/wallet';
import { ISignedMessage } from 'libs/signing'; import { ISignedMessage } from 'libs/signing';
import { IFullWallet } from 'libs/wallet'; import { IFullWallet } from 'libs/wallet';
@ -15,18 +15,17 @@ import { TextArea, CodeBlock } from 'components/ui';
interface Props { interface Props {
wallet: IFullWallet; wallet: IFullWallet;
unlocked: boolean; unlocked: boolean;
showNotification: TShowNotification; signMessageRequested: TSignMessageRequested;
signedMessage: ISignedMessage | null;
resetWallet: TResetWallet; resetWallet: TResetWallet;
} }
interface State { interface State {
message: string; message: string;
signedMessage: ISignedMessage | null;
} }
const initialState: State = { const initialState: State = {
message: '', message: ''
signedMessage: null
}; };
const messagePlaceholder = translateRaw('SIGN_MSG_PLACEHOLDER'); const messagePlaceholder = translateRaw('SIGN_MSG_PLACEHOLDER');
@ -39,8 +38,8 @@ export class SignMessage extends Component<Props, State> {
} }
public render() { public render() {
const { wallet, unlocked } = this.props; const { unlocked, signedMessage } = this.props;
const { message, signedMessage } = this.state; const { message } = this.state;
return ( return (
<div> <div>
@ -68,10 +67,8 @@ export class SignMessage extends Component<Props, State> {
</div> </div>
<SignButton <SignButton
wallet={wallet}
message={this.state.message} message={this.state.message}
showNotification={this.props.showNotification} signMessageRequested={this.props.signMessageRequested}
onSignMessage={this.onSignMessage}
/> />
{signedMessage && ( {signedMessage && (
@ -97,21 +94,17 @@ export class SignMessage extends Component<Props, State> {
this.setState({ message }); this.setState({ message });
}; };
private onSignMessage = (signedMessage: ISignedMessage) => {
this.setState({ signedMessage });
};
private changeWallet = () => { private changeWallet = () => {
this.props.resetWallet(); this.props.resetWallet();
}; };
} }
const mapStateToProps = (state: AppState) => ({ const mapStateToProps = (state: AppState) => ({
wallet: state.wallet.inst, signedMessage: state.message.signed,
unlocked: isWalletFullyUnlocked(state) unlocked: isWalletFullyUnlocked(state)
}); });
export default connect(mapStateToProps, { export default connect(mapStateToProps, {
showNotification, signMessageRequested,
resetWallet resetWallet
})(SignMessage); })(SignMessage);

View File

@ -92,7 +92,7 @@ export class VerifyMessage extends Component<Props, State> {
this.props.showNotification('success', translate('SUCCESS_7')); this.props.showNotification('success', translate('SUCCESS_7'));
} catch (err) { } catch (err) {
this.clearVerifiedData(); this.clearVerifiedData();
this.props.showNotification('danger', translate('ERROR_12')); this.props.showNotification('danger', translate('ERROR_38'));
} }
}; };

View File

@ -10,10 +10,9 @@ export default class ParitySignerWallet implements IFullWallet {
} }
public signRawTransaction = () => public signRawTransaction = () =>
Promise.reject(new Error('Web3 wallets cannot sign raw transactions.')); Promise.reject(new Error('Parity Signer cannot sign raw transactions.'));
public signMessage = () => public signMessage = () => Promise.reject(new Error('Parity Signer cannot sign messages.'));
Promise.reject(new Error('Signing via Parity Signer not yet supported.'));
public getAddressString() { public getAddressString() {
return this.address; return this.address;

View File

@ -9,6 +9,7 @@ import { rates, State as RatesState } from './rates';
import { State as SwapState, swap } from './swap'; import { State as SwapState, swap } from './swap';
import { State as WalletState, wallet } from './wallet'; import { State as WalletState, wallet } from './wallet';
import { State as TransactionState, transaction } from './transaction'; import { State as TransactionState, transaction } from './transaction';
import { State as MessageState, message } from './message';
import { State as GasState, gas } from './gas'; import { State as GasState, gas } from './gas';
import { onboardStatus, State as OnboardStatusState } from './onboardStatus'; import { onboardStatus, State as OnboardStatusState } from './onboardStatus';
import { State as TransactionsState, transactions } from './transactions'; import { State as TransactionsState, transactions } from './transactions';
@ -28,6 +29,7 @@ export interface AppState {
swap: SwapState; swap: SwapState;
transaction: TransactionState; transaction: TransactionState;
transactions: TransactionsState; transactions: TransactionsState;
message: MessageState;
paritySigner: ParitySignerState; paritySigner: ParitySignerState;
gas: GasState; gas: GasState;
schedule: ScheduleState; schedule: ScheduleState;
@ -47,6 +49,7 @@ export default combineReducers<AppState>({
deterministicWallets, deterministicWallets,
transaction, transaction,
transactions, transactions,
message,
paritySigner, paritySigner,
gas, gas,
schedule, schedule,

View File

@ -0,0 +1,35 @@
import { MessageAction, SignLocalMessageSucceededAction, TypeKeys } from 'actions/message';
import { ISignedMessage } from 'libs/signing';
export interface State {
signed?: ISignedMessage | null;
}
export const INITIAL_STATE: State = {
signed: null
};
function signLocalMessageSucceeded(state: State, action: SignLocalMessageSucceededAction): State {
return {
...state,
signed: action.payload
};
}
function signMessageFailed(state: State): State {
return {
...state,
signed: null
};
}
export function message(state: State = INITIAL_STATE, action: MessageAction): State {
switch (action.type) {
case TypeKeys.SIGN_LOCAL_MESSAGE_SUCCEEDED:
return signLocalMessageSucceeded(state, action);
case TypeKeys.SIGN_MESSAGE_FAILED:
return signMessageFailed(state);
default:
return state;
}
}

View File

@ -1,19 +1,35 @@
import { ParitySignerAction, RequestSignatureAction, TypeKeys } from 'actions/paritySigner'; import {
ParitySignerAction,
RequestTransactionSignatureAction,
RequestMessageSignatureAction,
TypeKeys
} from 'actions/paritySigner';
export interface State { export interface State {
requested?: QrSignatureState | null; requested?: QrSignatureState | null;
} }
interface QrSignatureState { interface QrSignatureState {
isMessage: boolean;
from: string; from: string;
rlp: string; data: string;
} }
export const INITIAL_STATE: State = { export const INITIAL_STATE: State = {
requested: null requested: null
}; };
function requestSignature(state: State, action: RequestSignatureAction): State { function requestTransactionSignature(
state: State,
action: RequestTransactionSignatureAction
): State {
return {
...state,
requested: action.payload
};
}
function requestMessageSignature(state: State, action: RequestMessageSignatureAction): State {
return { return {
...state, ...state,
requested: action.payload requested: action.payload
@ -29,8 +45,10 @@ function finalizeSignature(state: State): State {
export function paritySigner(state: State = INITIAL_STATE, action: ParitySignerAction): State { export function paritySigner(state: State = INITIAL_STATE, action: ParitySignerAction): State {
switch (action.type) { switch (action.type) {
case TypeKeys.PARITY_SIGNER_REQUEST_SIGNATURE: case TypeKeys.PARITY_SIGNER_REQUEST_TX_SIGNATURE:
return requestSignature(state, action); return requestTransactionSignature(state, action);
case TypeKeys.PARITY_SIGNER_REQUEST_MSG_SIGNATURE:
return requestMessageSignature(state, action);
case TypeKeys.PARITY_SIGNER_FINALIZE_SIGNATURE: case TypeKeys.PARITY_SIGNER_FINALIZE_SIGNATURE:
return finalizeSignature(state); return finalizeSignature(state);
default: default:

View File

@ -8,6 +8,7 @@ import swapRates from './swap/rates';
import wallet from './wallet'; import wallet from './wallet';
import { ens } from './ens'; import { ens } from './ens';
import { transaction } from './transaction'; import { transaction } from './transaction';
import { message } from './message';
import transactions from './transactions'; import transactions from './transactions';
import gas from './gas'; import gas from './gas';
import { schedule } from './schedule'; import { schedule } from './schedule';
@ -21,6 +22,7 @@ export default {
notifications, notifications,
wallet, wallet,
transaction, transaction,
message,
deterministicWallets, deterministicWallets,
rates, rates,
transactions, transactions,

View File

@ -0,0 +1,38 @@
import { SagaIterator } from 'redux-saga';
import { put, call, select } from 'redux-saga/effects';
import translate from 'translations';
import { padLeftEven } from 'libs/values';
import { showNotification } from 'actions/notifications';
import { getWalletInst } from 'selectors/wallet';
import { IFullWallet } from 'libs/wallet';
import { signMessageFailed, SignMessageRequestedAction } from 'actions/message';
export function* signingWrapper(
handler: (wallet: IFullWallet, message: string) => SagaIterator,
action: SignMessageRequestedAction
): SagaIterator {
const message = action.payload;
const wallet = yield select(getWalletInst);
try {
yield call(handler, wallet, message);
} catch (err) {
yield put(showNotification('danger', translate('SIGN_MSG_FAIL', { $err: err.message }), 5000));
yield put(signMessageFailed());
}
}
/**
* Turns a string into hex-encoded UTF-8 byte array, `0x` prefixed.
*
* @param {string} message to encode
* @return {string}
*/
export function messageToData(message: string): string {
return (
'0x' +
Array.from(Buffer.from(message, 'utf8'))
.map(n => padLeftEven(n.toString(16)))
.join('')
);
}

View File

@ -0,0 +1,7 @@
import { SagaIterator } from 'redux-saga';
import { all } from 'redux-saga/effects';
import { signing } from './signing';
export function* message(): SagaIterator {
yield all([...signing]);
}

View File

@ -0,0 +1,89 @@
import { SagaIterator } from 'redux-saga';
import { put, take, apply, takeEvery, call, select } from 'redux-saga/effects';
import translate, { translateRaw } from 'translations';
import { showNotification } from 'actions/notifications';
import { verifySignedMessage } from 'libs/signing';
import {
TypeKeys,
SignMessageRequestedAction,
signLocalMessageSucceeded,
SignLocalMessageSucceededAction,
signMessageFailed
} from 'actions/message';
import {
requestMessageSignature,
FinalizeSignatureAction,
TypeKeys as ParityKeys
} from 'actions/paritySigner';
import { IFullWallet } from 'libs/wallet';
import { getWalletType, IWalletType } from 'selectors/wallet';
import { messageToData, signingWrapper } from './helpers';
function* signLocalMessage(wallet: IFullWallet, msg: string): SagaIterator {
const address = yield apply(wallet, wallet.getAddressString);
const sig: string = yield apply(wallet, wallet.signMessage, [msg]);
yield put(
signLocalMessageSucceeded({
address,
msg,
sig,
version: '2'
})
);
}
function* signParitySignerMessage(wallet: IFullWallet, msg: string): SagaIterator {
const address = yield apply(wallet, wallet.getAddressString);
const data = yield call(messageToData, msg);
yield put(requestMessageSignature(address, data));
const { payload: sig }: FinalizeSignatureAction = yield take(
ParityKeys.PARITY_SIGNER_FINALIZE_SIGNATURE
);
if (!sig) {
throw new Error(translateRaw('ERROR_38'));
}
yield put(
signLocalMessageSucceeded({
address,
msg,
sig,
version: '2'
})
);
}
function* handleMessageRequest(action: SignMessageRequestedAction): SagaIterator {
const walletType: IWalletType = yield select(getWalletType);
const signingHandler = walletType.isParitySignerWallet
? signParitySignerMessage
: signLocalMessage;
return yield call(signingWrapper, signingHandler, action);
}
function* verifySignature(action: SignLocalMessageSucceededAction): SagaIterator {
const success = yield call(verifySignedMessage, action.payload);
if (success) {
yield put(
showNotification(
'success',
translate('SIGN_MSG_SUCCESS', { $address: action.payload.address })
)
);
} else {
yield put(signMessageFailed());
yield put(showNotification('danger', translate('ERROR_38')));
}
}
export const signing = [
takeEvery(TypeKeys.SIGN_MESSAGE_REQUESTED, handleMessageRequest),
takeEvery(TypeKeys.SIGN_LOCAL_MESSAGE_SUCCEEDED, verifySignature)
];

View File

@ -15,7 +15,7 @@ import { computeIndexingHash } from 'libs/transaction';
import { serializedAndTransactionFieldsMatch } from 'selectors/transaction'; import { serializedAndTransactionFieldsMatch } from 'selectors/transaction';
import { showNotification } from 'actions/notifications'; import { showNotification } from 'actions/notifications';
import { import {
requestSignature, requestTransactionSignature,
FinalizeSignatureAction, FinalizeSignatureAction,
TypeKeys as ParityKeys TypeKeys as ParityKeys
} from 'actions/paritySigner'; } from 'actions/paritySigner';
@ -53,7 +53,7 @@ export function* signParitySignerTransactionHandler({
const from = yield apply(wallet, wallet.getAddressString); const from = yield apply(wallet, wallet.getAddressString);
const rlp = yield call(transactionToRLP, tx); const rlp = yield call(transactionToRLP, tx);
yield put(requestSignature(from, rlp)); yield put(requestTransactionSignature(from, rlp));
const { payload }: FinalizeSignatureAction = yield take( const { payload }: FinalizeSignatureAction = yield take(
ParityKeys.PARITY_SIGNER_FINALIZE_SIGNATURE ParityKeys.PARITY_SIGNER_FINALIZE_SIGNATURE

View File

@ -266,6 +266,7 @@
"ERROR_34": "The name you are attempting to reveal does not match the name you have entered. ", "ERROR_34": "The name you are attempting to reveal does not match the name you have entered. ",
"ERROR_36": "Enter valid TX hash", "ERROR_36": "Enter valid TX hash",
"ERROR_37": "Enter valid hex string (0-9, a-f)", "ERROR_37": "Enter valid hex string (0-9, a-f)",
"ERROR_38": "Invalid signed message. ",
"SUCCESS_1": "Valid address ", "SUCCESS_1": "Valid address ",
"SUCCESS_2": "Wallet successfully decrypted ", "SUCCESS_2": "Wallet successfully decrypted ",
"SUCCESS_3": "Your TX has been broadcast to the network. It is waiting to be mined & confirmed. During ICOs, it may take 3+ hours to confirm. Use the Verify & Check buttons below to see. TX Hash: ", "SUCCESS_3": "Your TX has been broadcast to the network. It is waiting to be mined & confirmed. During ICOs, it may take 3+ hours to confirm. Use the Verify & Check buttons below to see. TX Hash: ",

View File

@ -10,7 +10,7 @@
"npm": ">= 5.0.0" "npm": ">= 5.0.0"
}, },
"dependencies": { "dependencies": {
"@parity/qr-signer": "0.1.1", "@parity/qr-signer": "0.2.0",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"bip39": "2.5.0", "bip39": "2.5.0",
"bn.js": "4.11.8", "bn.js": "4.11.8",