UI support for hardware wallets (#4539)
* Add parity_hardwareAccountsInfo * Ledger Promise interface wrapper * Initial hardwarestore * Move ~/views/historyStore to ~/mobx * split scanLedger * test createEntry * Also scan via parity_hardwareAccountsInfo * Explanation for scanning options * react-intl-inify tooltips * add hwstore * Listen for hw walet updates * Return arrays from scanning * Readability * add u2f-api polyfill * check response.errorCode * Support hardware types in state.personal * Tooltips (to be split into sep. PR) * Tooltips support intl strings * FormattedMessage for strings to Tooltip * Fix TabBar tooltip display * signLedger * Use wallets as an object map * PendingForm -> FormattedMessage * Pending form doesn't render password for hardware * Groundwork for JS API signing * Show hardware accounts in list * Cleanup rendering conditions * Update RequestPending rendering tests (verification) * Tests for extended signer middleware * sign properly & handle response, error * Align outputs between Parity & Ledger u2f * Ledger returns checksummed addresses * Update ethereum-tx for EIP155 support * Update construction of tx * Updates after sanity checks (thanks @tomusdrw) * Allow display for disabled IdentityIcon * Disabled accounts * Disabled auto-disabling * Password button ebaled for hardware * Don't display password hint for hardware * Disable non-applicable options when not connected * Fix failing test * Confirmation via ledger (u2f) * Confirm on device message * Cleanups & support checks * Mark u2f as unsupported (until https) * rewording * Pass account & disabled flags * Render attach device message * Use isConnected for checking availability * Show hardware accounts in defaults list * Pass signerstore * Update u2f to correct version * remove debug u2f lib * Update test (prop name change) * Add ETC path (future work) * new Buffer -> Buffer.from (thanks @derhuerst)
This commit is contained in:
parent
96d74543fc
commit
b11caaf071
@ -153,7 +153,7 @@
|
||||
"debounce": "1.0.0",
|
||||
"es6-error": "4.0.0",
|
||||
"es6-promise": "4.0.5",
|
||||
"ethereumjs-tx": "1.1.4",
|
||||
"ethereumjs-tx": "1.2.5",
|
||||
"eventemitter3": "2.0.2",
|
||||
"file-saver": "1.3.3",
|
||||
"flat": "2.0.1",
|
||||
@ -200,6 +200,8 @@
|
||||
"scryptsy": "2.0.0",
|
||||
"solc": "ngotchac/solc-js",
|
||||
"store": "1.3.20",
|
||||
"u2f-api": "0.0.9",
|
||||
"u2f-api-polyfill": "0.4.3",
|
||||
"uglify-js": "2.8.2",
|
||||
"useragent.js": "0.5.6",
|
||||
"utf8": "2.1.2",
|
||||
|
10
js/src/3rdparty/ledger/index.js
vendored
10
js/src/3rdparty/ledger/index.js
vendored
@ -14,12 +14,4 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Ledger3 from './vendor/ledger3';
|
||||
import LedgerEth from './vendor/ledger-eth';
|
||||
|
||||
export function create () {
|
||||
const ledger = new Ledger3('w0w');
|
||||
const app = new LedgerEth(ledger);
|
||||
|
||||
return app;
|
||||
}
|
||||
export default from './ledger';
|
||||
|
136
js/src/3rdparty/ledger/ledger.js
vendored
Normal file
136
js/src/3rdparty/ledger/ledger.js
vendored
Normal file
@ -0,0 +1,136 @@
|
||||
// Copyright 2015-2017 Parity Technologies (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 'u2f-api-polyfill';
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import Transaction from 'ethereumjs-tx';
|
||||
import u2fapi from 'u2f-api';
|
||||
|
||||
import Ledger3 from './vendor/ledger3';
|
||||
import LedgerEth from './vendor/ledger-eth';
|
||||
|
||||
const LEDGER_PATH_ETC = "44’/60’/160720'/0'/0";
|
||||
const LEDGER_PATH_ETH = "44'/60'/0'/0";
|
||||
const SCRAMBLE_KEY = 'w0w';
|
||||
|
||||
function numberToHex (number) {
|
||||
return `0x${new BigNumber(number).toString(16)}`;
|
||||
}
|
||||
|
||||
export default class Ledger {
|
||||
constructor (api, ledger) {
|
||||
this._api = api;
|
||||
this._ledger = ledger;
|
||||
|
||||
this._isSupported = false;
|
||||
|
||||
this.checkJSSupport();
|
||||
}
|
||||
|
||||
// FIXME: Until we have https support from Parity u2f will not work. Here we mark it completely
|
||||
// as unsupported until a full end-to-end environment is available.
|
||||
get isSupported () {
|
||||
return false && this._isSupported;
|
||||
}
|
||||
|
||||
checkJSSupport () {
|
||||
return u2fapi
|
||||
.isSupported()
|
||||
.then((isSupported) => {
|
||||
console.log('Ledger:checkJSSupport', isSupported);
|
||||
|
||||
this._isSupported = isSupported;
|
||||
});
|
||||
}
|
||||
|
||||
getAppConfiguration () {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._ledger.getAppConfiguration((response, error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
scan () {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._ledger.getAddress(LEDGER_PATH_ETH, (response, error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve([response.address]);
|
||||
}, true, false);
|
||||
});
|
||||
}
|
||||
|
||||
signTransaction (transaction) {
|
||||
return this._api.net.version().then((_chainId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chainId = new BigNumber(_chainId).toNumber();
|
||||
const tx = new Transaction({
|
||||
data: transaction.data || transaction.input,
|
||||
gasPrice: numberToHex(transaction.gasPrice),
|
||||
gasLimit: numberToHex(transaction.gasLimit),
|
||||
nonce: numberToHex(transaction.nonce),
|
||||
to: transaction.to ? transaction.to.toLowerCase() : undefined,
|
||||
value: numberToHex(transaction.value),
|
||||
v: Buffer.from([chainId]), // pass the chainId to the ledger
|
||||
r: Buffer.from([]),
|
||||
s: Buffer.from([])
|
||||
});
|
||||
const rawTransaction = tx.serialize().toString('hex');
|
||||
|
||||
this._ledger.signTransaction(LEDGER_PATH_ETH, rawTransaction, (response, error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
tx.v = Buffer.from(response.v, 'hex');
|
||||
tx.r = Buffer.from(response.r, 'hex');
|
||||
tx.s = Buffer.from(response.s, 'hex');
|
||||
|
||||
if (chainId !== Math.floor((tx.v[0] - 35) / 2)) {
|
||||
reject(new Error('Invalid EIP155 signature received from Ledger.'));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(`0x${tx.serialize().toString('hex')}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static create (api, ledger) {
|
||||
if (!ledger) {
|
||||
ledger = new LedgerEth(new Ledger3(SCRAMBLE_KEY));
|
||||
}
|
||||
|
||||
return new Ledger(api, ledger);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
LEDGER_PATH_ETC,
|
||||
LEDGER_PATH_ETH
|
||||
};
|
120
js/src/3rdparty/ledger/ledger.spec.js
vendored
Normal file
120
js/src/3rdparty/ledger/ledger.spec.js
vendored
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright 2015-2017 Parity Technologies (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 sinon from 'sinon';
|
||||
|
||||
import Ledger from './';
|
||||
|
||||
const TEST_ADDRESS = '0x63Cf90D3f0410092FC0fca41846f596223979195';
|
||||
|
||||
let api;
|
||||
let ledger;
|
||||
let vendor;
|
||||
|
||||
function createApi () {
|
||||
api = {
|
||||
net: {
|
||||
version: sinon.stub().resolves('2')
|
||||
}
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function createVendor (error = null) {
|
||||
vendor = {
|
||||
getAddress: (path, callback) => {
|
||||
callback({
|
||||
address: TEST_ADDRESS
|
||||
}, error);
|
||||
},
|
||||
getAppConfiguration: (callback) => {
|
||||
callback({}, error);
|
||||
},
|
||||
signTransaction: (path, rawTransaction, callback) => {
|
||||
callback({
|
||||
v: [39],
|
||||
r: [0],
|
||||
s: [0]
|
||||
}, error);
|
||||
}
|
||||
};
|
||||
|
||||
return vendor;
|
||||
}
|
||||
|
||||
function create (error) {
|
||||
ledger = new Ledger(createApi(), createVendor(error));
|
||||
|
||||
return ledger;
|
||||
}
|
||||
|
||||
describe('3rdparty/ledger', () => {
|
||||
beforeEach(() => {
|
||||
create();
|
||||
|
||||
sinon.spy(vendor, 'getAddress');
|
||||
sinon.spy(vendor, 'getAppConfiguration');
|
||||
sinon.spy(vendor, 'signTransaction');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vendor.getAddress.restore();
|
||||
vendor.getAppConfiguration.restore();
|
||||
vendor.signTransaction.restore();
|
||||
});
|
||||
|
||||
describe('getAppConfiguration', () => {
|
||||
beforeEach(() => {
|
||||
return ledger.getAppConfiguration();
|
||||
});
|
||||
|
||||
it('calls into getAppConfiguration', () => {
|
||||
expect(vendor.getAppConfiguration).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('scan', () => {
|
||||
beforeEach(() => {
|
||||
return ledger.scan();
|
||||
});
|
||||
|
||||
it('calls into getAddress', () => {
|
||||
expect(vendor.getAddress).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('signTransaction', () => {
|
||||
beforeEach(() => {
|
||||
return ledger.signTransaction({
|
||||
data: '0x0',
|
||||
gasPrice: 20000000,
|
||||
gasLimit: 1000000,
|
||||
nonce: 2,
|
||||
to: '0x63Cf90D3f0410092FC0fca41846f596223979195',
|
||||
value: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves chainId via API', () => {
|
||||
expect(api.net.version).to.have.been.called;
|
||||
});
|
||||
|
||||
it('calls into signTransaction', () => {
|
||||
expect(vendor.signTransaction).to.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
@ -128,6 +128,18 @@ export function outLog (log) {
|
||||
return log;
|
||||
}
|
||||
|
||||
export function outHwAccountInfo (infos) {
|
||||
return Object
|
||||
.keys(infos)
|
||||
.reduce((ret, _address) => {
|
||||
const address = outAddress(_address);
|
||||
|
||||
ret[address] = infos[_address];
|
||||
|
||||
return ret;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function outNumber (number) {
|
||||
return new BigNumber(number || 0);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
import { outBlock, outAccountInfo, outAddress, outChainStatus, outDate, outHistogram, outNumber, outPeer, outPeers, outReceipt, outRecentDapps, outSyncing, outTransaction, outTrace, outVaultMeta } from './output';
|
||||
import { outBlock, outAccountInfo, outAddress, outChainStatus, outDate, outHistogram, outHwAccountInfo, outNumber, outPeer, outPeers, outReceipt, outRecentDapps, outSyncing, outTransaction, outTrace, outVaultMeta } from './output';
|
||||
import { isAddress, isBigNumber, isInstanceOf } from '../../../test/types';
|
||||
|
||||
describe('api/format/output', () => {
|
||||
@ -163,6 +163,16 @@ describe('api/format/output', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('outHwAccountInfo', () => {
|
||||
it('returns objects with formatted addresses', () => {
|
||||
expect(outHwAccountInfo(
|
||||
{ '0x63cf90d3f0410092fc0fca41846f596223979195': { manufacturer: 'mfg', name: 'type' } }
|
||||
)).to.deep.equal({
|
||||
'0x63Cf90D3f0410092FC0fca41846f596223979195': { manufacturer: 'mfg', name: 'type' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('outNumber', () => {
|
||||
it('returns a BigNumber equalling the value', () => {
|
||||
const bn = outNumber('0x123456');
|
||||
|
@ -15,7 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions, inBlockNumber } from '../../format/input';
|
||||
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outRecentDapps, outTransaction, outVaultMeta } from '../../format/output';
|
||||
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outHwAccountInfo, outNumber, outPeers, outRecentDapps, outTransaction, outVaultMeta } from '../../format/output';
|
||||
|
||||
export default class Parity {
|
||||
constructor (transport) {
|
||||
@ -200,6 +200,12 @@ export default class Parity {
|
||||
.then(outVaultMeta);
|
||||
}
|
||||
|
||||
hardwareAccountsInfo () {
|
||||
return this._transport
|
||||
.execute('parity_hardwareAccountsInfo')
|
||||
.then(outHwAccountInfo);
|
||||
}
|
||||
|
||||
hashContent (url) {
|
||||
return this._transport
|
||||
.execute('parity_hashContent', url);
|
||||
|
@ -393,6 +393,32 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
hardwareAccountsInfo: {
|
||||
section: SECTION_ACCOUNTS,
|
||||
desc: 'Provides metadata for attached hardware wallets',
|
||||
params: [],
|
||||
returns: {
|
||||
type: Object,
|
||||
desc: 'Maps account address to metadata.',
|
||||
details: {
|
||||
manufacturer: {
|
||||
type: String,
|
||||
desc: 'Manufacturer'
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
desc: 'Account name'
|
||||
}
|
||||
},
|
||||
example: {
|
||||
'0x0024d0c7ab4c52f723f3aaf0872b9ea4406846a4': {
|
||||
manufacturer: 'Ledger',
|
||||
name: 'Nano S'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
listOpenedVaults: {
|
||||
desc: 'Returns a list of all opened vaults',
|
||||
params: [],
|
||||
|
159
js/src/mobx/hardwareStore.js
Normal file
159
js/src/mobx/hardwareStore.js
Normal file
@ -0,0 +1,159 @@
|
||||
// Copyright 2015-2017 Parity Technologies (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 { action, computed, observable, transaction } from 'mobx';
|
||||
|
||||
import Ledger from '~/3rdparty/ledger';
|
||||
|
||||
const HW_SCAN_INTERVAL = 5000;
|
||||
let instance = null;
|
||||
|
||||
export default class HardwareStore {
|
||||
@observable isScanning = false;
|
||||
@observable wallets = {};
|
||||
|
||||
constructor (api) {
|
||||
this._api = api;
|
||||
this._ledger = Ledger.create(api);
|
||||
this._pollId = null;
|
||||
|
||||
this._pollScan();
|
||||
}
|
||||
|
||||
isConnected (address) {
|
||||
return computed(() => !!this.wallets[address]).get();
|
||||
}
|
||||
|
||||
@action setScanning = (isScanning) => {
|
||||
this.isScanning = isScanning;
|
||||
}
|
||||
|
||||
@action setWallets = (wallets) => {
|
||||
this.wallets = wallets;
|
||||
}
|
||||
|
||||
_pollScan = () => {
|
||||
this._pollId = setTimeout(() => {
|
||||
this.scan().then(this._pollScan);
|
||||
}, HW_SCAN_INTERVAL);
|
||||
}
|
||||
|
||||
scanLedger () {
|
||||
if (!this._ledger.isSupported) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
return this._ledger
|
||||
.scan()
|
||||
.then((wallets) => {
|
||||
console.log('HardwareStore::scanLedger', wallets);
|
||||
|
||||
return wallets.reduce((hwInfo, wallet) => {
|
||||
wallet.manufacturer = 'Ledger';
|
||||
wallet.name = 'Nano S';
|
||||
wallet.via = 'ledger';
|
||||
|
||||
hwInfo[wallet.address] = wallet;
|
||||
|
||||
return hwInfo;
|
||||
}, {});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('HardwareStore::scanLedger', error);
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
scanParity () {
|
||||
return this._api.parity
|
||||
.hardwareAccountsInfo()
|
||||
.then((hwInfo) => {
|
||||
Object
|
||||
.keys(hwInfo)
|
||||
.forEach((address) => {
|
||||
const info = hwInfo[address];
|
||||
|
||||
info.address = address;
|
||||
info.via = 'parity';
|
||||
});
|
||||
|
||||
return hwInfo;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('HardwareStore::scanParity', error);
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
scan () {
|
||||
this.setScanning(true);
|
||||
|
||||
// NOTE: Depending on how the hardware is configured and how the local env setup
|
||||
// is done, different results will be retrieved via Parity vs. the browser APIs
|
||||
// (latter is Chrome-only, needs the browser app enabled on a Ledger, former is
|
||||
// not intended as a network call, i.e. hw wallet is with the user)
|
||||
return Promise
|
||||
.all([
|
||||
this.scanParity(),
|
||||
this.scanLedger()
|
||||
])
|
||||
.then(([hwAccounts, ledgerAccounts]) => {
|
||||
transaction(() => {
|
||||
this.setWallets(Object.assign({}, hwAccounts, ledgerAccounts));
|
||||
this.setScanning(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createAccountInfo (entry) {
|
||||
const { address, manufacturer, name } = entry;
|
||||
|
||||
return Promise
|
||||
.all([
|
||||
this._api.parity.setAccountName(address, name),
|
||||
this._api.parity.setAccountMeta(address, {
|
||||
description: `${manufacturer} ${name}`,
|
||||
hardware: {
|
||||
manufacturer
|
||||
},
|
||||
tags: ['hardware'],
|
||||
timestamp: Date.now()
|
||||
})
|
||||
])
|
||||
.catch((error) => {
|
||||
console.warn('HardwareStore::createEntry', error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
signLedger (transaction) {
|
||||
return this._ledger.signTransaction(transaction);
|
||||
}
|
||||
|
||||
static get (api) {
|
||||
if (!instance) {
|
||||
instance = new HardwareStore(api);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
HW_SCAN_INTERVAL
|
||||
};
|
220
js/src/mobx/hardwareStore.spec.js
Normal file
220
js/src/mobx/hardwareStore.spec.js
Normal file
@ -0,0 +1,220 @@
|
||||
// Copyright 2015-2017 Parity Technologies (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 sinon from 'sinon';
|
||||
|
||||
import HardwareStore, { HW_SCAN_INTERVAL } from './hardwareStore';
|
||||
|
||||
const ADDRESS = '0x1234567890123456789012345678901234567890';
|
||||
const WALLET = {
|
||||
address: ADDRESS,
|
||||
name: 'testing'
|
||||
};
|
||||
|
||||
let api;
|
||||
let clock;
|
||||
let ledger;
|
||||
let store;
|
||||
|
||||
function createApi () {
|
||||
api = {
|
||||
parity: {
|
||||
hardwareAccountsInfo: sinon.stub().resolves({ ADDRESS: WALLET }),
|
||||
setAccountMeta: sinon.stub().resolves(true),
|
||||
setAccountName: sinon.stub().resolves(true)
|
||||
}
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function createLedger () {
|
||||
ledger = {
|
||||
isSupported: true,
|
||||
getAppConfiguration: sinon.stub().resolves(),
|
||||
scan: sinon.stub().resolves(WALLET),
|
||||
signTransaction: sinon.stub().resolves()
|
||||
};
|
||||
|
||||
return ledger;
|
||||
}
|
||||
|
||||
function create () {
|
||||
clock = sinon.useFakeTimers();
|
||||
store = new HardwareStore(createApi());
|
||||
store._ledger = createLedger();
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
function teardown () {
|
||||
clock.restore();
|
||||
}
|
||||
|
||||
describe('mobx/HardwareStore', () => {
|
||||
beforeEach(() => {
|
||||
create();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
teardown();
|
||||
});
|
||||
|
||||
describe('@computed', () => {
|
||||
describe('isConnected', () => {
|
||||
beforeEach(() => {
|
||||
store.setWallets({ [ADDRESS]: WALLET });
|
||||
});
|
||||
|
||||
it('returns true for available', () => {
|
||||
expect(store.isConnected(ADDRESS)).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false for non-available', () => {
|
||||
expect(store.isConnected('nothing')).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('background polling', () => {
|
||||
let pollId;
|
||||
|
||||
beforeEach(() => {
|
||||
pollId = store._pollId;
|
||||
sinon.spy(store, 'scan');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.scan.restore();
|
||||
});
|
||||
|
||||
it('starts the polling at creation', () => {
|
||||
expect(pollId).not.to.be.null;
|
||||
});
|
||||
|
||||
it('scans when timer elapsed', () => {
|
||||
expect(store.scan).not.to.have.been.called;
|
||||
clock.tick(HW_SCAN_INTERVAL + 1);
|
||||
expect(store.scan).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('@action', () => {
|
||||
describe('setScanning', () => {
|
||||
it('sets the flag', () => {
|
||||
store.setScanning('testScanning');
|
||||
expect(store.isScanning).to.equal('testScanning');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setWallets', () => {
|
||||
it('sets the wallets', () => {
|
||||
store.setWallets('testWallet');
|
||||
expect(store.wallets).to.equal('testWallet');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('operations', () => {
|
||||
describe('createAccountInfo', () => {
|
||||
beforeEach(() => {
|
||||
return store.createAccountInfo({
|
||||
address: 'testAddr',
|
||||
manufacturer: 'testMfg',
|
||||
name: 'testName'
|
||||
});
|
||||
});
|
||||
|
||||
it('calls into parity_setAccountName', () => {
|
||||
expect(api.parity.setAccountName).to.have.been.calledWith('testAddr', 'testName');
|
||||
});
|
||||
|
||||
it('calls into parity_setAccountMeta', () => {
|
||||
expect(api.parity.setAccountMeta).to.have.been.calledWith('testAddr', sinon.match({
|
||||
description: 'testMfg testName',
|
||||
hardware: {
|
||||
manufacturer: 'testMfg'
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('scanLedger', () => {
|
||||
beforeEach(() => {
|
||||
return store.scanLedger();
|
||||
});
|
||||
|
||||
it('calls scan on the Ledger APIs', () => {
|
||||
expect(ledger.scan).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('scanParity', () => {
|
||||
beforeEach(() => {
|
||||
return store.scanParity();
|
||||
});
|
||||
|
||||
it('calls parity_hardwareAccountsInfo', () => {
|
||||
expect(api.parity.hardwareAccountsInfo).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('scan', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(store, 'setScanning');
|
||||
sinon.spy(store, 'setWallets');
|
||||
sinon.spy(store, 'scanLedger');
|
||||
sinon.spy(store, 'scanParity');
|
||||
|
||||
return store.scan();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.setScanning.restore();
|
||||
store.setWallets.restore();
|
||||
store.scanLedger.restore();
|
||||
store.scanParity.restore();
|
||||
});
|
||||
|
||||
it('calls scanLedger', () => {
|
||||
expect(store.scanLedger).to.have.been.called;
|
||||
});
|
||||
|
||||
it('calls scanParity', () => {
|
||||
expect(store.scanParity).to.have.been.called;
|
||||
});
|
||||
|
||||
it('sets and resets the scanning state', () => {
|
||||
expect(store.setScanning).to.have.been.calledWith(true);
|
||||
expect(store.setScanning).to.have.been.calledWith(false);
|
||||
});
|
||||
|
||||
it('sets the wallets', () => {
|
||||
expect(store.setWallets).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('signLedger', () => {
|
||||
beforeEach(() => {
|
||||
return store.signLedger('testTx');
|
||||
});
|
||||
|
||||
it('calls signTransaction on the ledger', () => {
|
||||
expect(ledger.signTransaction).to.have.been.calledWith('testTx');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -29,7 +29,7 @@ function create () {
|
||||
return store;
|
||||
}
|
||||
|
||||
describe('views/HistoryStore', () => {
|
||||
describe('mobx/HistoryStore', () => {
|
||||
beforeEach(() => {
|
||||
create();
|
||||
});
|
@ -30,6 +30,7 @@ export function personalAccountsInfo (accountsInfo) {
|
||||
const accounts = {};
|
||||
const contacts = {};
|
||||
const contracts = {};
|
||||
const hardware = {};
|
||||
const wallets = {};
|
||||
|
||||
Object.keys(accountsInfo || {})
|
||||
@ -43,7 +44,12 @@ export function personalAccountsInfo (accountsInfo) {
|
||||
account.wallet = true;
|
||||
wallets[account.address] = account;
|
||||
} else if (account.meta.contract) {
|
||||
account.contract = true;
|
||||
contracts[account.address] = account;
|
||||
} else if (account.meta.hardware) {
|
||||
account.hardware = true;
|
||||
hardware[account.address] = account;
|
||||
accounts[account.address] = account;
|
||||
} else {
|
||||
contacts[account.address] = account;
|
||||
}
|
||||
@ -93,12 +99,13 @@ export function personalAccountsInfo (accountsInfo) {
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
dispatch(_personalAccountsInfo({
|
||||
accountsInfo,
|
||||
accounts, contacts, contracts
|
||||
};
|
||||
|
||||
dispatch(_personalAccountsInfo(data));
|
||||
accounts,
|
||||
contacts,
|
||||
contracts,
|
||||
hardware
|
||||
}));
|
||||
dispatch(attachWallets(wallets));
|
||||
|
||||
BalancesProvider.get().fetchAllBalances({
|
||||
|
@ -14,33 +14,36 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { isEqual } from 'lodash';
|
||||
import { handleActions } from 'redux-actions';
|
||||
|
||||
const initialState = {
|
||||
accountsInfo: {},
|
||||
accounts: {},
|
||||
hasAccounts: false,
|
||||
contacts: {},
|
||||
hasContacts: false,
|
||||
contracts: {},
|
||||
hardware: {},
|
||||
hasAccounts: false,
|
||||
hasContacts: false,
|
||||
hasContracts: false,
|
||||
hasHardware: false,
|
||||
visibleAccounts: []
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
personalAccountsInfo (state, action) {
|
||||
const accountsInfo = action.accountsInfo || state.accountsInfo;
|
||||
const { accounts, contacts, contracts } = action;
|
||||
const { accounts, contacts, contracts, hardware } = action;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
accountsInfo,
|
||||
accounts,
|
||||
hasAccounts: Object.keys(accounts).length !== 0,
|
||||
contacts,
|
||||
hasContacts: Object.keys(contacts).length !== 0,
|
||||
contracts,
|
||||
hasContracts: Object.keys(contracts).length !== 0
|
||||
hasAccounts: Object.keys(accounts).length !== 0,
|
||||
hasContacts: Object.keys(contacts).length !== 0,
|
||||
hasContracts: Object.keys(contracts).length !== 0,
|
||||
hasHardware: Object.keys(hardware).length !== 0
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -17,11 +17,13 @@
|
||||
import * as actions from './signerActions';
|
||||
|
||||
import { inHex } from '~/api/format/input';
|
||||
import { Signer } from '../../util/signer';
|
||||
import HardwareStore from '~/mobx/hardwareStore';
|
||||
import { Signer } from '~/util/signer';
|
||||
|
||||
export default class SignerMiddleware {
|
||||
constructor (api) {
|
||||
this._api = api;
|
||||
this._hwstore = HardwareStore.get(api);
|
||||
}
|
||||
|
||||
toMiddleware () {
|
||||
@ -51,11 +53,9 @@ export default class SignerMiddleware {
|
||||
};
|
||||
}
|
||||
|
||||
onConfirmStart = (store, action) => {
|
||||
const { condition, gas = 0, gasPrice = 0, id, password, payload, wallet } = action.payload;
|
||||
|
||||
const handlePromise = (promise) => {
|
||||
promise
|
||||
_createConfirmPromiseHandler (store, id) {
|
||||
return (promise) => {
|
||||
return promise
|
||||
.then((txHash) => {
|
||||
if (!txHash) {
|
||||
store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' }));
|
||||
@ -69,15 +69,31 @@ export default class SignerMiddleware {
|
||||
store.dispatch(actions.errorConfirmRequest({ id, err: error.message }));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Sign request in-browser
|
||||
const transaction = payload.sendTransaction || payload.signTransaction;
|
||||
|
||||
if (wallet && transaction) {
|
||||
const noncePromise = transaction.nonce.isZero()
|
||||
createNoncePromise (transaction) {
|
||||
return !transaction.nonce || transaction.nonce.isZero()
|
||||
? this._api.parity.nextNonce(transaction.from)
|
||||
: Promise.resolve(transaction.nonce);
|
||||
}
|
||||
|
||||
confirmLedgerTransaction (store, id, transaction) {
|
||||
return this
|
||||
.createNoncePromise(transaction)
|
||||
.then((nonce) => {
|
||||
transaction.nonce = nonce;
|
||||
|
||||
return this._hwstore.signLedger(transaction);
|
||||
})
|
||||
.then((rawTx) => {
|
||||
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
||||
|
||||
return handlePromise(this._api.signer.confirmRequestRaw(id, rawTx));
|
||||
});
|
||||
}
|
||||
|
||||
confirmWalletTransaction (store, id, transaction, wallet, password) {
|
||||
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
||||
const { worker } = store.getState().worker;
|
||||
|
||||
const signerPromise = worker && worker._worker.state === 'activated'
|
||||
@ -96,7 +112,7 @@ export default class SignerMiddleware {
|
||||
// NOTE: Derving the key takes significant amount of time,
|
||||
// make sure to display some kind of "in-progress" state.
|
||||
return Promise
|
||||
.all([ signerPromise, noncePromise ])
|
||||
.all([ signerPromise, this.createNoncePromise(transaction) ])
|
||||
.then(([ signer, nonce ]) => {
|
||||
const txData = {
|
||||
to: inHex(transaction.to),
|
||||
@ -118,13 +134,35 @@ export default class SignerMiddleware {
|
||||
});
|
||||
}
|
||||
|
||||
handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice, condition }, password));
|
||||
onConfirmStart = (store, action) => {
|
||||
const { condition, gas = 0, gasPrice = 0, id, password, payload, wallet } = action.payload;
|
||||
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
||||
const transaction = payload.sendTransaction || payload.signTransaction;
|
||||
|
||||
if (transaction) {
|
||||
const hardwareAccount = this._hwstore.wallets[transaction.from];
|
||||
|
||||
if (wallet) {
|
||||
return this.confirmWalletTransaction(store, id, transaction, wallet, password);
|
||||
} else if (hardwareAccount) {
|
||||
switch (hardwareAccount.via) {
|
||||
case 'ledger':
|
||||
return this.confirmLedgerTransaction(store, id, transaction);
|
||||
|
||||
case 'parity':
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice, condition }, password));
|
||||
}
|
||||
|
||||
onRejectStart = (store, action) => {
|
||||
const id = action.payload;
|
||||
|
||||
this._api.signer
|
||||
return this._api.signer
|
||||
.rejectRequest(id)
|
||||
.then(() => {
|
||||
store.dispatch(actions.successRejectRequest({ id }));
|
||||
|
221
js/src/redux/providers/signerMiddleware.spec.js
Normal file
221
js/src/redux/providers/signerMiddleware.spec.js
Normal file
@ -0,0 +1,221 @@
|
||||
// Copyright 2015-2017 Parity Technologies (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 BigNumber from 'bignumber.js';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import SignerMiddleware from './signerMiddleware';
|
||||
|
||||
const ADDRESS = '0x3456789012345678901234567890123456789012';
|
||||
const RAW_SIGNED = 'testSignResponse';
|
||||
const NONCE = new BigNumber(0x123454321);
|
||||
const TRANSACTION = {
|
||||
from: ADDRESS,
|
||||
nonce: NONCE
|
||||
};
|
||||
const PAYLOAD = {
|
||||
condition: 'testCondition',
|
||||
gas: 'testGas',
|
||||
gasPrice: 'testGasPrice',
|
||||
id: 'testId',
|
||||
password: 'testPassword',
|
||||
payload: {
|
||||
sendTransaction: TRANSACTION
|
||||
}
|
||||
};
|
||||
const ACTION = {
|
||||
payload: PAYLOAD
|
||||
};
|
||||
|
||||
let api;
|
||||
let clock;
|
||||
let hwstore;
|
||||
let middleware;
|
||||
let store;
|
||||
|
||||
function createApi () {
|
||||
api = {
|
||||
net: {
|
||||
version: sinon.stub().resolves('2')
|
||||
},
|
||||
parity: {
|
||||
nextNonce: sinon.stub().resolves(NONCE)
|
||||
},
|
||||
signer: {
|
||||
confirmRequest: sinon.stub().resolves(true),
|
||||
confirmRequestRaw: sinon.stub().resolves(true),
|
||||
rejectRequest: sinon.stub().resolves(true)
|
||||
}
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function createHwStore () {
|
||||
hwstore = {
|
||||
signLedger: sinon.stub().resolves(RAW_SIGNED),
|
||||
wallets: {
|
||||
[ADDRESS]: {
|
||||
address: ADDRESS,
|
||||
via: 'ledger'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return hwstore;
|
||||
}
|
||||
|
||||
function createRedux () {
|
||||
return {
|
||||
dispatch: sinon.stub(),
|
||||
getState: () => {
|
||||
return {
|
||||
worker: {
|
||||
worker: null
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function create () {
|
||||
clock = sinon.useFakeTimers();
|
||||
store = createRedux();
|
||||
middleware = new SignerMiddleware(createApi());
|
||||
|
||||
return middleware;
|
||||
}
|
||||
|
||||
function teardown () {
|
||||
clock.restore();
|
||||
}
|
||||
|
||||
describe('redux/SignerMiddleware', () => {
|
||||
beforeEach(() => {
|
||||
create();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
teardown();
|
||||
});
|
||||
|
||||
describe('createNoncePromise', () => {
|
||||
it('resolves via transaction.nonce when available', () => {
|
||||
const nonce = new BigNumber('0xabc');
|
||||
|
||||
return middleware.createNoncePromise({ nonce }).then((_nonce) => {
|
||||
expect(_nonce).to.equal(nonce);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls parity_nextNonce', () => {
|
||||
return middleware.createNoncePromise({ from: 'testing' }).then((nonce) => {
|
||||
expect(api.parity.nextNonce).to.have.been.calledWith('testing');
|
||||
expect(nonce).to.equal(NONCE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('confirmLedgerTransaction', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(middleware, 'createNoncePromise');
|
||||
middleware._hwstore = createHwStore();
|
||||
|
||||
return middleware.confirmLedgerTransaction(store, PAYLOAD.id, TRANSACTION);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
middleware.createNoncePromise.restore();
|
||||
});
|
||||
|
||||
it('creates nonce via createNoncePromise', () => {
|
||||
expect(middleware.createNoncePromise).to.have.been.calledWith(TRANSACTION);
|
||||
});
|
||||
|
||||
it('calls into hardware signLedger', () => {
|
||||
expect(hwstore.signLedger).to.have.been.calledWith(TRANSACTION);
|
||||
});
|
||||
|
||||
it('confirms via signer_confirmRequestRaw', () => {
|
||||
expect(api.signer.confirmRequestRaw).to.have.been.calledWith(PAYLOAD.id, RAW_SIGNED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onConfirmStart', () => {
|
||||
describe('normal accounts', () => {
|
||||
beforeEach(() => {
|
||||
return middleware.onConfirmStart(store, ACTION);
|
||||
});
|
||||
|
||||
it('calls into signer_confirmRequest', () => {
|
||||
expect(api.signer.confirmRequest).to.have.been.calledWith(
|
||||
PAYLOAD.id,
|
||||
{
|
||||
condition: PAYLOAD.condition,
|
||||
gas: PAYLOAD.gas,
|
||||
gasPrice: PAYLOAD.gasPrice
|
||||
},
|
||||
PAYLOAD.password
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hardware accounts', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(middleware, 'confirmLedgerTransaction');
|
||||
middleware._hwstore = createHwStore();
|
||||
|
||||
return middleware.onConfirmStart(store, ACTION);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
middleware.confirmLedgerTransaction.restore();
|
||||
});
|
||||
|
||||
it('calls out to confirmLedgerTransaction', () => {
|
||||
expect(middleware.confirmLedgerTransaction).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('json wallet accounts', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(middleware, 'confirmWalletTransaction');
|
||||
|
||||
return middleware.onConfirmStart(store, {
|
||||
payload: Object.assign({}, PAYLOAD, { wallet: 'testWallet' })
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
middleware.confirmWalletTransaction.restore();
|
||||
});
|
||||
|
||||
it('calls out to confirmWalletTransaction', () => {
|
||||
expect(middleware.confirmWalletTransaction).to.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onRejectStart', () => {
|
||||
beforeEach(() => {
|
||||
return middleware.onRejectStart(store, { payload: 'testId' });
|
||||
});
|
||||
|
||||
it('calls into signer_rejectRequest', () => {
|
||||
expect(api.signer.rejectRequest).to.have.been.calledWith('testId');
|
||||
});
|
||||
});
|
||||
});
|
@ -14,9 +14,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import HistoryStore from '~/mobx/historyStore';
|
||||
import {
|
||||
Accounts, Account, Addresses, Address, Application,
|
||||
Contract, Contracts, Dapp, Dapps, HistoryStore, Home,
|
||||
Contract, Contracts, Dapp, Dapps, Home,
|
||||
Settings, SettingsBackground, SettingsParity, SettingsProxy,
|
||||
SettingsViews, Signer, Status,
|
||||
Vaults, Wallet, Web, WriteContract
|
||||
|
@ -14,6 +14,7 @@
|
||||
/* You should have received a copy of the GNU General Public License
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.actionbar {
|
||||
padding: 0 24px !important;
|
||||
margin-bottom: 0;
|
||||
@ -31,10 +32,13 @@
|
||||
|
||||
button {
|
||||
margin: 10px 0 10px 16px !important;
|
||||
|
||||
&:not([disabled]) {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,15 @@
|
||||
/* You should have received a copy of the GNU General Public License
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.icon {
|
||||
border-radius: 50%;
|
||||
margin: 0;
|
||||
|
||||
&.disabled {
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.33;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
|
@ -33,6 +33,7 @@ class IdentityIcon extends Component {
|
||||
button: PropTypes.bool,
|
||||
center: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
images: PropTypes.object.isRequired,
|
||||
inline: PropTypes.bool,
|
||||
padded: PropTypes.bool,
|
||||
@ -83,10 +84,11 @@ class IdentityIcon extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { address, button, className, center, inline, padded, tiny } = this.props;
|
||||
const { address, button, className, center, disabled, inline, padded, tiny } = this.props;
|
||||
const { iconsrc } = this.state;
|
||||
const classes = [
|
||||
styles.icon,
|
||||
disabled ? styles.disabled : '',
|
||||
tiny ? styles.tiny : '',
|
||||
button ? styles.button : '',
|
||||
center ? styles.center : styles.left,
|
||||
|
@ -27,6 +27,7 @@ export default class Header extends Component {
|
||||
balance: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hideName: PropTypes.bool,
|
||||
isContract: PropTypes.bool
|
||||
};
|
||||
@ -39,7 +40,7 @@ export default class Header extends Component {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, balance, children, className, hideName } = this.props;
|
||||
const { account, balance, children, className, disabled, hideName } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
@ -58,12 +59,15 @@ export default class Header extends Component {
|
||||
<IdentityIcon
|
||||
address={ address }
|
||||
className={ styles.identityIcon }
|
||||
disabled={ disabled }
|
||||
/>
|
||||
<div className={ styles.info }>
|
||||
{ this.renderName() }
|
||||
<div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }>
|
||||
<CopyToClipboard data={ address } />
|
||||
<div className={ styles.address }>{ address }</div>
|
||||
<div className={ styles.address }>
|
||||
{ address }
|
||||
</div>
|
||||
</div>
|
||||
{ this.renderVault() }
|
||||
{ this.renderUuid() }
|
||||
|
@ -21,12 +21,15 @@ import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
|
||||
import HardwareStore from '~/mobx/hardwareStore';
|
||||
import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals';
|
||||
import { setVisibleAccounts } from '~/redux/providers/personalActions';
|
||||
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
|
||||
import { Actionbar, Button, Page } from '~/ui';
|
||||
import { DeleteIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons';
|
||||
|
||||
import DeleteAddress from '../Address/Delete';
|
||||
|
||||
import Header from './Header';
|
||||
import Store from './store';
|
||||
import Transactions from './Transactions';
|
||||
@ -34,6 +37,10 @@ import styles from './account.css';
|
||||
|
||||
@observer
|
||||
class Account extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
fetchCertifiers: PropTypes.func.isRequired,
|
||||
fetchCertifications: PropTypes.func.isRequired,
|
||||
@ -45,6 +52,7 @@ class Account extends Component {
|
||||
}
|
||||
|
||||
store = new Store();
|
||||
hwstore = HardwareStore.get(this.context.api);
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchCertifiers();
|
||||
@ -83,6 +91,8 @@ class Account extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAvailable = !account.hardware || this.hwstore.isConnected(address);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this.renderDeleteDialog(account) }
|
||||
@ -91,11 +101,12 @@ class Account extends Component {
|
||||
{ this.renderPasswordDialog(account) }
|
||||
{ this.renderTransferDialog(account, balance) }
|
||||
{ this.renderVerificationDialog() }
|
||||
{ this.renderActionbar(balance) }
|
||||
{ this.renderActionbar(account, balance) }
|
||||
<Page padded>
|
||||
<Header
|
||||
account={ account }
|
||||
balance={ balance }
|
||||
disabled={ !isAvailable }
|
||||
/>
|
||||
<Transactions
|
||||
accounts={ accounts }
|
||||
@ -106,7 +117,7 @@ class Account extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderActionbar (balance) {
|
||||
renderActionbar (account, balance) {
|
||||
const showTransferButton = !!(balance && balance.tokens);
|
||||
|
||||
const buttons = [
|
||||
@ -160,6 +171,7 @@ class Account extends Component {
|
||||
}
|
||||
onClick={ this.store.toggleEditDialog }
|
||||
/>,
|
||||
!account.hardware && (
|
||||
<Button
|
||||
icon={ <LockedIcon /> }
|
||||
key='passwordManager'
|
||||
@ -170,7 +182,8 @@ class Account extends Component {
|
||||
/>
|
||||
}
|
||||
onClick={ this.store.togglePasswordDialog }
|
||||
/>,
|
||||
/>
|
||||
),
|
||||
<Button
|
||||
icon={ <DeleteIcon /> }
|
||||
key='delete'
|
||||
@ -202,6 +215,23 @@ class Account extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (account.hardware) {
|
||||
return (
|
||||
<DeleteAddress
|
||||
account={ account }
|
||||
confirmMessage={
|
||||
<FormattedMessage
|
||||
id='account.hardware.confirmDelete'
|
||||
defaultMessage='Are you sure you want to remove the following hardware address from your account list?'
|
||||
/>
|
||||
}
|
||||
visible
|
||||
route='/accounts'
|
||||
onClose={ this.store.toggleDeleteDialog }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DeleteAccount
|
||||
account={ account }
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { ADDRESS, createRedux } from './account.test.js';
|
||||
import { ACCOUNTS, ADDRESS, createRedux } from './account.test.js';
|
||||
|
||||
import Account from './';
|
||||
|
||||
@ -28,10 +28,15 @@ let store;
|
||||
function render (props) {
|
||||
component = shallow(
|
||||
<Account
|
||||
accounts={ ACCOUNTS }
|
||||
params={ { address: ADDRESS } }
|
||||
{ ...props }
|
||||
/>,
|
||||
{ context: { store: createRedux() } }
|
||||
{
|
||||
context: {
|
||||
store: createRedux()
|
||||
}
|
||||
}
|
||||
).find('Account').shallow();
|
||||
instance = component.instance();
|
||||
store = instance.store;
|
||||
@ -133,14 +138,14 @@ describe('views/Account', () => {
|
||||
render();
|
||||
|
||||
expect(store.isDeleteVisible).to.be.false;
|
||||
expect(instance.renderDeleteDialog()).to.be.null;
|
||||
expect(instance.renderDeleteDialog(ACCOUNTS[ADDRESS])).to.be.null;
|
||||
});
|
||||
|
||||
it('renders the modal when visible', () => {
|
||||
render();
|
||||
|
||||
store.toggleDeleteDialog();
|
||||
expect(instance.renderDeleteDialog().type).to.match(/Connect/);
|
||||
expect(instance.renderDeleteDialog(ACCOUNTS[ADDRESS]).type).to.match(/Connect/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -149,14 +154,14 @@ describe('views/Account', () => {
|
||||
render();
|
||||
|
||||
expect(store.isEditVisible).to.be.false;
|
||||
expect(instance.renderEditDialog()).to.be.null;
|
||||
expect(instance.renderEditDialog(ACCOUNTS[ADDRESS])).to.be.null;
|
||||
});
|
||||
|
||||
it('renders the modal when visible', () => {
|
||||
render();
|
||||
|
||||
store.toggleEditDialog();
|
||||
expect(instance.renderEditDialog({ address: ADDRESS }).type).to.match(/Connect/);
|
||||
expect(instance.renderEditDialog(ACCOUNTS[ADDRESS]).type).to.match(/Connect/);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -17,6 +17,11 @@
|
||||
import sinon from 'sinon';
|
||||
|
||||
const ADDRESS = '0x0123456789012345678901234567890123456789';
|
||||
const ACCOUNTS = {
|
||||
[ADDRESS]: {
|
||||
address: ADDRESS
|
||||
}
|
||||
};
|
||||
|
||||
function createRedux () {
|
||||
return {
|
||||
@ -47,6 +52,7 @@ function createRedux () {
|
||||
}
|
||||
|
||||
export {
|
||||
ACCOUNTS,
|
||||
ADDRESS,
|
||||
createRedux
|
||||
};
|
||||
|
@ -29,6 +29,7 @@ class List extends Component {
|
||||
accounts: PropTypes.object,
|
||||
balances: PropTypes.object,
|
||||
certifications: PropTypes.object.isRequired,
|
||||
disabled: PropTypes.object,
|
||||
empty: PropTypes.bool,
|
||||
link: PropTypes.string,
|
||||
order: PropTypes.string,
|
||||
@ -50,7 +51,7 @@ class List extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accounts, balances, empty } = this.props;
|
||||
const { accounts, balances, disabled, empty } = this.props;
|
||||
|
||||
if (empty) {
|
||||
return (
|
||||
@ -64,14 +65,16 @@ class List extends Component {
|
||||
|
||||
const addresses = this
|
||||
.getAddresses()
|
||||
.map((address, idx) => {
|
||||
.map((address) => {
|
||||
const account = accounts[address] || {};
|
||||
const balance = balances[address] || {};
|
||||
const isDisabled = disabled ? disabled[address] : false;
|
||||
const owners = account.owners || null;
|
||||
|
||||
return {
|
||||
account,
|
||||
balance,
|
||||
isDisabled,
|
||||
owners
|
||||
};
|
||||
});
|
||||
@ -85,13 +88,14 @@ class List extends Component {
|
||||
}
|
||||
|
||||
renderSummary = (item) => {
|
||||
const { account, balance, owners } = item;
|
||||
const { account, balance, isDisabled, owners } = item;
|
||||
const { handleAddSearchToken, link } = this.props;
|
||||
|
||||
return (
|
||||
<Summary
|
||||
account={ account }
|
||||
balance={ balance }
|
||||
disabled={ isDisabled }
|
||||
handleAddSearchToken={ handleAddSearchToken }
|
||||
link={ link }
|
||||
owners={ owners }
|
||||
|
@ -37,6 +37,7 @@ class Summary extends Component {
|
||||
account: PropTypes.object.isRequired,
|
||||
accountsInfo: PropTypes.object.isRequired,
|
||||
balance: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
link: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
noLink: PropTypes.bool,
|
||||
@ -52,15 +53,21 @@ class Summary extends Component {
|
||||
|
||||
shouldComponentUpdate (nextProps) {
|
||||
const prev = {
|
||||
link: this.props.link, name: this.props.name,
|
||||
link: this.props.link,
|
||||
disabled: this.props.disabled,
|
||||
name: this.props.name,
|
||||
noLink: this.props.noLink,
|
||||
meta: this.props.account.meta, address: this.props.account.address
|
||||
meta: this.props.account.meta,
|
||||
address: this.props.account.address
|
||||
};
|
||||
|
||||
const next = {
|
||||
link: nextProps.link, name: nextProps.name,
|
||||
link: nextProps.link,
|
||||
disabled: nextProps.disabled,
|
||||
name: nextProps.name,
|
||||
noLink: nextProps.noLink,
|
||||
meta: nextProps.account.meta, address: nextProps.account.address
|
||||
meta: nextProps.account.meta,
|
||||
address: nextProps.account.address
|
||||
};
|
||||
|
||||
if (!isEqual(next, prev)) {
|
||||
@ -92,7 +99,7 @@ class Summary extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, handleAddSearchToken, noLink } = this.props;
|
||||
const { account, disabled, handleAddSearchToken, noLink } = this.props;
|
||||
const { tags } = account.meta;
|
||||
|
||||
if (!account) {
|
||||
@ -122,6 +129,7 @@ class Summary extends Component {
|
||||
<div className={ styles.heading }>
|
||||
<IdentityIcon
|
||||
address={ address }
|
||||
disabled={ disabled }
|
||||
/>
|
||||
<ContainerTitle
|
||||
byline={
|
||||
|
@ -14,34 +14,42 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { uniq, isEqual, pickBy, omitBy } from 'lodash';
|
||||
import { observe } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { uniq, isEqual, pickBy } from 'lodash';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import List from './List';
|
||||
import HardwareStore from '~/mobx/hardwareStore';
|
||||
import { CreateAccount, CreateWallet } from '~/modals';
|
||||
import { Actionbar, ActionbarExport, ActionbarSearch, ActionbarSort, Button, Page, Tooltip } from '~/ui';
|
||||
import { AddIcon, KeyIcon } from '~/ui/Icons';
|
||||
import { setVisibleAccounts } from '~/redux/providers/personalActions';
|
||||
|
||||
import List from './List';
|
||||
import styles from './accounts.css';
|
||||
|
||||
@observer
|
||||
class Accounts extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
setVisibleAccounts: PropTypes.func.isRequired,
|
||||
accounts: PropTypes.object.isRequired,
|
||||
accountsInfo: PropTypes.object.isRequired,
|
||||
balances: PropTypes.object,
|
||||
hasAccounts: PropTypes.bool.isRequired,
|
||||
balances: PropTypes.object
|
||||
setVisibleAccounts: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
hwstore = HardwareStore.get(this.context.api);
|
||||
|
||||
state = {
|
||||
_observeCancel: null,
|
||||
addressBook: false,
|
||||
newDialog: false,
|
||||
newWalletDialog: false,
|
||||
@ -58,6 +66,10 @@ class Accounts extends Component {
|
||||
}, 100);
|
||||
|
||||
this.setVisibleAccounts();
|
||||
|
||||
this.setState({
|
||||
_observeCancel: observe(this.hwstore, 'wallets', this.onHardwareChange, true)
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@ -71,13 +83,13 @@ class Accounts extends Component {
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.setVisibleAccounts([]);
|
||||
this.state._observeCancel();
|
||||
}
|
||||
|
||||
setVisibleAccounts (props = this.props) {
|
||||
const { accounts, setVisibleAccounts } = props;
|
||||
const addresses = Object.keys(accounts);
|
||||
|
||||
setVisibleAccounts(addresses);
|
||||
setVisibleAccounts(Object.keys(accounts));
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -98,6 +110,7 @@ class Accounts extends Component {
|
||||
}
|
||||
/>
|
||||
|
||||
{ this.renderHwWallets() }
|
||||
{ this.renderWallets() }
|
||||
{ this.renderAccounts() }
|
||||
</Page>
|
||||
@ -121,8 +134,7 @@ class Accounts extends Component {
|
||||
|
||||
renderAccounts () {
|
||||
const { accounts, balances } = this.props;
|
||||
|
||||
const _accounts = omitBy(accounts, (a) => a.wallet);
|
||||
const _accounts = pickBy(accounts, (account) => account.uuid);
|
||||
const _hasAccounts = Object.keys(_accounts).length > 0;
|
||||
|
||||
if (!this.state.show) {
|
||||
@ -145,27 +157,60 @@ class Accounts extends Component {
|
||||
|
||||
renderWallets () {
|
||||
const { accounts, balances } = this.props;
|
||||
|
||||
const wallets = pickBy(accounts, (a) => a.wallet);
|
||||
const wallets = pickBy(accounts, (account) => account.wallet);
|
||||
const hasWallets = Object.keys(wallets).length > 0;
|
||||
|
||||
if (!hasWallets) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.state.show) {
|
||||
return this.renderLoading(wallets);
|
||||
}
|
||||
|
||||
const { searchValues, sortOrder } = this.state;
|
||||
|
||||
if (!wallets || Object.keys(wallets).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
link='wallet'
|
||||
search={ searchValues }
|
||||
accounts={ wallets }
|
||||
balances={ balances }
|
||||
empty={ !hasWallets }
|
||||
order={ sortOrder }
|
||||
handleAddSearchToken={ this.onAddSearchToken }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderHwWallets () {
|
||||
const { accounts, balances } = this.props;
|
||||
const { wallets } = this.hwstore;
|
||||
const hardware = pickBy(accounts, (account) => account.hardware);
|
||||
const hasHardware = Object.keys(hardware).length > 0;
|
||||
|
||||
if (!hasHardware) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.state.show) {
|
||||
return this.renderLoading(hardware);
|
||||
}
|
||||
|
||||
const { searchValues, sortOrder } = this.state;
|
||||
const disabled = Object
|
||||
.keys(hardware)
|
||||
.filter((address) => !wallets[address])
|
||||
.reduce((result, address) => {
|
||||
result[address] = true;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<List
|
||||
search={ searchValues }
|
||||
accounts={ hardware }
|
||||
balances={ balances }
|
||||
disabled={ disabled }
|
||||
order={ sortOrder }
|
||||
handleAddSearchToken={ this.onAddSearchToken }
|
||||
/>
|
||||
@ -342,16 +387,29 @@ class Accounts extends Component {
|
||||
|
||||
onNewAccountUpdate = () => {
|
||||
}
|
||||
|
||||
onHardwareChange = () => {
|
||||
const { accountsInfo } = this.props;
|
||||
const { wallets } = this.hwstore;
|
||||
|
||||
Object
|
||||
.keys(wallets)
|
||||
.filter((address) => !accountsInfo[address])
|
||||
.forEach((address) => this.hwstore.createAccountInfo(wallets[address]));
|
||||
|
||||
this.setVisibleAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { accounts, hasAccounts } = state.personal;
|
||||
const { accounts, accountsInfo, hasAccounts } = state.personal;
|
||||
const { balances } = state.balances;
|
||||
|
||||
return {
|
||||
accounts: accounts,
|
||||
hasAccounts: hasAccounts,
|
||||
balances
|
||||
accounts,
|
||||
accountsInfo,
|
||||
balances,
|
||||
hasAccounts
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -35,13 +35,14 @@ class Delete extends Component {
|
||||
|
||||
address: PropTypes.string,
|
||||
account: PropTypes.object,
|
||||
confirmMessage: PropTypes.node,
|
||||
visible: PropTypes.bool,
|
||||
onClose: PropTypes.func,
|
||||
newError: PropTypes.func
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, visible } = this.props;
|
||||
const { account, confirmMessage, visible } = this.props;
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
@ -61,10 +62,14 @@ class Delete extends Component {
|
||||
onConfirm={ this.onDeleteConfirmed }
|
||||
>
|
||||
<div className={ styles.hero }>
|
||||
{
|
||||
confirmMessage || (
|
||||
<FormattedMessage
|
||||
id='address.delete.confirmInfo'
|
||||
defaultMessage='Are you sure you want to remove the following address from your addressbook?'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className={ styles.info }>
|
||||
<IdentityIcon
|
||||
@ -112,15 +117,11 @@ class Delete extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return bindActionCreators({ newError }, dispatch);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(Delete);
|
||||
|
@ -18,11 +18,11 @@ import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import HistoryStore from '~/mobx/historyStore';
|
||||
import { Page } from '~/ui';
|
||||
|
||||
import DappsStore from '../Dapps/dappsStore';
|
||||
import ExtensionStore from '../Application/Extension/store';
|
||||
import HistoryStore from '../historyStore';
|
||||
import WebStore from '../Web/store';
|
||||
|
||||
import Accounts from './Accounts';
|
||||
|
@ -81,7 +81,8 @@ export default class AccountStore {
|
||||
Object
|
||||
.keys(accounts)
|
||||
.filter((address) => {
|
||||
const isAccount = accounts[address].uuid;
|
||||
const account = accounts[address];
|
||||
const isAccount = account.uuid || (account.meta && account.meta.hardware);
|
||||
const isWhitelisted = !whitelist || whitelist.includes(address);
|
||||
|
||||
return isAccount && isWhitelisted;
|
||||
|
@ -23,8 +23,9 @@ import styles from './account.css';
|
||||
|
||||
export default class Account extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
address: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
externalLink: PropTypes.string.isRequired,
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
balance: PropTypes.object // eth BigNumber, not required since it mght take time to fetch
|
||||
@ -52,7 +53,7 @@ export default class Account extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { address, externalLink, isTest, className } = this.props;
|
||||
const { address, disabled, externalLink, isTest, className } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ `${styles.acc} ${className}` }>
|
||||
@ -63,6 +64,7 @@ export default class Account extends Component {
|
||||
>
|
||||
<IdentityIcon
|
||||
center
|
||||
disabled={ disabled }
|
||||
address={ address }
|
||||
/>
|
||||
</AccountLink>
|
||||
|
@ -36,7 +36,7 @@ export default class RequestPending extends Component {
|
||||
PropTypes.shape({ sign: PropTypes.object.isRequired }),
|
||||
PropTypes.shape({ signTransaction: PropTypes.object.isRequired })
|
||||
]).isRequired,
|
||||
store: PropTypes.object.isRequired
|
||||
signerstore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -44,15 +44,8 @@ export default class RequestPending extends Component {
|
||||
isSending: false
|
||||
};
|
||||
|
||||
onConfirm = data => {
|
||||
const { onConfirm, payload } = this.props;
|
||||
|
||||
data.payload = payload;
|
||||
onConfirm(data);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { className, date, focus, gasLimit, id, isSending, isTest, onReject, payload, store, origin } = this.props;
|
||||
const { className, date, focus, gasLimit, id, isSending, isTest, onReject, payload, signerstore, origin } = this.props;
|
||||
|
||||
if (payload.sign) {
|
||||
const { sign } = payload;
|
||||
@ -70,7 +63,7 @@ export default class RequestPending extends Component {
|
||||
onConfirm={ this.onConfirm }
|
||||
onReject={ onReject }
|
||||
origin={ origin }
|
||||
store={ store }
|
||||
signerstore={ signerstore }
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -90,7 +83,7 @@ export default class RequestPending extends Component {
|
||||
onConfirm={ this.onConfirm }
|
||||
onReject={ onReject }
|
||||
origin={ origin }
|
||||
store={ store }
|
||||
signerstore={ signerstore }
|
||||
transaction={ transaction }
|
||||
/>
|
||||
);
|
||||
@ -99,4 +92,11 @@ export default class RequestPending extends Component {
|
||||
console.error('RequestPending: Unknown payload', payload);
|
||||
return null;
|
||||
}
|
||||
|
||||
onConfirm = (data) => {
|
||||
const { onConfirm, payload } = this.props;
|
||||
|
||||
data.payload = payload;
|
||||
onConfirm(data);
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,112 @@
|
||||
// Copyright 2015-2017 Parity Technologies (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 BigNumber from 'bignumber.js';
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import RequestPending from './';
|
||||
|
||||
const ADDRESS = '0x1234567890123456789012345678901234567890';
|
||||
const TRANSACTION = {
|
||||
from: ADDRESS,
|
||||
gas: new BigNumber(21000),
|
||||
gasPrice: new BigNumber(20000000),
|
||||
value: new BigNumber(1)
|
||||
};
|
||||
const PAYLOAD_SENDTX = {
|
||||
sendTransaction: TRANSACTION
|
||||
};
|
||||
const PAYLOAD_SIGN = {
|
||||
sign: {
|
||||
address: ADDRESS,
|
||||
data: 'testing'
|
||||
}
|
||||
};
|
||||
const PAYLOAD_SIGNTX = {
|
||||
signTransaction: TRANSACTION
|
||||
};
|
||||
|
||||
let component;
|
||||
let onConfirm;
|
||||
let onReject;
|
||||
|
||||
function render (payload) {
|
||||
onConfirm = sinon.stub();
|
||||
onReject = sinon.stub();
|
||||
|
||||
component = shallow(
|
||||
<RequestPending
|
||||
date={ new Date() }
|
||||
gasLimit={ new BigNumber(100000) }
|
||||
id={ new BigNumber(123) }
|
||||
isTest={ false }
|
||||
isSending={ false }
|
||||
onConfirm={ onConfirm }
|
||||
onReject={ onReject }
|
||||
origin={ {} }
|
||||
payload={ payload }
|
||||
store={ {} }
|
||||
/>
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('views/Signer/RequestPending', () => {
|
||||
describe('sendTransaction', () => {
|
||||
beforeEach(() => {
|
||||
render(PAYLOAD_SENDTX);
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
it('renders TransactionPending component', () => {
|
||||
expect(component.find('Connect(TransactionPending)')).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sign', () => {
|
||||
beforeEach(() => {
|
||||
render(PAYLOAD_SIGN);
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
it('renders SignRequest component', () => {
|
||||
expect(component.find('SignRequest')).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('signTransaction', () => {
|
||||
beforeEach(() => {
|
||||
render(PAYLOAD_SIGNTX);
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
it('renders TransactionPending component', () => {
|
||||
expect(component.find('Connect(TransactionPending)')).to.have.length(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -47,7 +47,7 @@ export default class SignRequest extends Component {
|
||||
id: PropTypes.object.isRequired,
|
||||
isFinished: PropTypes.bool.isRequired,
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
store: PropTypes.object.isRequired,
|
||||
signerstore: PropTypes.object.isRequired,
|
||||
|
||||
className: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
@ -67,9 +67,9 @@ export default class SignRequest extends Component {
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { address, store } = this.props;
|
||||
const { address, signerstore } = this.props;
|
||||
|
||||
store.fetchBalance(address);
|
||||
signerstore.fetchBalance(address);
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -106,8 +106,8 @@ export default class SignRequest extends Component {
|
||||
|
||||
renderDetails () {
|
||||
const { api } = this.context;
|
||||
const { address, isTest, store, data, origin } = this.props;
|
||||
const { balances, externalLink } = store;
|
||||
const { address, isTest, signerstore, data, origin } = this.props;
|
||||
const { balances, externalLink } = signerstore;
|
||||
|
||||
const balance = balances[address];
|
||||
|
||||
|
@ -28,7 +28,7 @@ const store = {
|
||||
describe('views/Signer/components/SignRequest', () => {
|
||||
it('renders', () => {
|
||||
expect(shallow(
|
||||
<SignRequest store={ store } />,
|
||||
<SignRequest signerstore={ store } />,
|
||||
)).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
@ -30,6 +30,7 @@ import styles from './transactionMainDetails.css';
|
||||
export default class TransactionMainDetails extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
disabled: PropTypes.bool,
|
||||
externalLink: PropTypes.string.isRequired,
|
||||
from: PropTypes.string.isRequired,
|
||||
fromBalance: PropTypes.object,
|
||||
@ -62,7 +63,7 @@ export default class TransactionMainDetails extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, externalLink, from, fromBalance, gasStore, isTest, transaction, origin } = this.props;
|
||||
const { children, disabled, externalLink, from, fromBalance, gasStore, isTest, transaction, origin } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ styles.transaction }>
|
||||
@ -71,6 +72,7 @@ export default class TransactionMainDetails extends Component {
|
||||
<Account
|
||||
address={ from }
|
||||
balance={ fromBalance }
|
||||
disabled={ disabled }
|
||||
externalLink={ externalLink }
|
||||
isTest={ isTest }
|
||||
/>
|
||||
|
@ -14,10 +14,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { observer } from 'mobx-react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import HardwareStore from '~/mobx/hardwareStore';
|
||||
import { Button, GasPriceEditor } from '~/ui';
|
||||
|
||||
import TransactionMainDetails from '../TransactionMainDetails';
|
||||
@ -28,12 +30,13 @@ import styles from './transactionPending.css';
|
||||
import * as tUtil from '../util/transaction';
|
||||
|
||||
@observer
|
||||
export default class TransactionPending extends Component {
|
||||
class TransactionPending extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
accounts: PropTypes.object.isRequired,
|
||||
className: PropTypes.string,
|
||||
date: PropTypes.instanceOf(Date).isRequired,
|
||||
focus: PropTypes.bool,
|
||||
@ -45,7 +48,7 @@ export default class TransactionPending extends Component {
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onReject: PropTypes.func.isRequired,
|
||||
origin: PropTypes.any,
|
||||
store: PropTypes.object.isRequired,
|
||||
signerstore: PropTypes.object.isRequired,
|
||||
transaction: PropTypes.shape({
|
||||
condition: PropTypes.object,
|
||||
data: PropTypes.string,
|
||||
@ -72,8 +75,10 @@ export default class TransactionPending extends Component {
|
||||
gasPrice: this.props.transaction.gasPrice.toFixed()
|
||||
});
|
||||
|
||||
hwstore = HardwareStore.get(this.context.api);
|
||||
|
||||
componentWillMount () {
|
||||
const { store, transaction } = this.props;
|
||||
const { signerstore, transaction } = this.props;
|
||||
const { from, gas, gasPrice, to, value } = transaction;
|
||||
|
||||
const fee = tUtil.getFee(gas, gasPrice); // BigNumber object
|
||||
@ -83,7 +88,7 @@ export default class TransactionPending extends Component {
|
||||
|
||||
this.setState({ gasPriceEthmDisplay, totalValue, gasToDisplay });
|
||||
this.gasStore.setEthValue(value);
|
||||
store.fetchBalances([from, to]);
|
||||
signerstore.fetchBalances([from, to]);
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -93,17 +98,19 @@ export default class TransactionPending extends Component {
|
||||
}
|
||||
|
||||
renderTransaction () {
|
||||
const { className, focus, id, isSending, isTest, store, transaction, origin } = this.props;
|
||||
const { accounts, className, focus, id, isSending, isTest, signerstore, transaction, origin } = this.props;
|
||||
const { totalValue } = this.state;
|
||||
const { balances, externalLink } = store;
|
||||
const { balances, externalLink } = signerstore;
|
||||
const { from, value } = transaction;
|
||||
|
||||
const fromBalance = balances[from];
|
||||
const account = accounts[from] || {};
|
||||
const disabled = account.hardware && !this.hwstore.isConnected(from);
|
||||
|
||||
return (
|
||||
<div className={ `${styles.container} ${className}` }>
|
||||
<TransactionMainDetails
|
||||
className={ styles.transactionDetails }
|
||||
disabled={ disabled }
|
||||
externalLink={ externalLink }
|
||||
from={ from }
|
||||
fromBalance={ fromBalance }
|
||||
@ -116,7 +123,9 @@ export default class TransactionPending extends Component {
|
||||
value={ value }
|
||||
/>
|
||||
<TransactionPendingForm
|
||||
account={ account }
|
||||
address={ from }
|
||||
disabled={ disabled }
|
||||
focus={ focus }
|
||||
isSending={ isSending }
|
||||
onConfirm={ this.onConfirm }
|
||||
@ -174,3 +183,16 @@ export default class TransactionPending extends Component {
|
||||
this.gasStore.setEditing(false);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { accounts } = state.personal;
|
||||
|
||||
return {
|
||||
accounts
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(TransactionPending);
|
||||
|
@ -19,17 +19,17 @@ import RaisedButton from 'material-ui/RaisedButton';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
import { Form, Input, IdentityIcon } from '~/ui';
|
||||
|
||||
import styles from './transactionPendingFormConfirm.css';
|
||||
|
||||
class TransactionPendingFormConfirm extends Component {
|
||||
export default class TransactionPendingFormConfirm extends Component {
|
||||
static propTypes = {
|
||||
account: PropTypes.object.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
isSending: PropTypes.bool.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
focus: PropTypes.bool
|
||||
@ -93,74 +93,16 @@ class TransactionPendingFormConfirm extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, address, isSending } = this.props;
|
||||
const { password, wallet, walletError } = this.state;
|
||||
const isExternal = !account.uuid;
|
||||
|
||||
const passwordHintText = this.getPasswordHint();
|
||||
const passwordHint = passwordHintText
|
||||
? (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.passwordHint'
|
||||
defaultMessage='(hint) {hint}'
|
||||
values={ {
|
||||
hint: passwordHintText
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
|
||||
const isWalletOk = !isExternal || (walletError === null && wallet !== null);
|
||||
const keyInput = isExternal
|
||||
? this.renderKeyInput()
|
||||
: null;
|
||||
const { account, address, disabled, isSending } = this.props;
|
||||
const { wallet, walletError } = this.state;
|
||||
const isWalletOk = account.hardware || account.uuid || (walletError === null && wallet !== null);
|
||||
|
||||
return (
|
||||
<div className={ styles.confirmForm }>
|
||||
<Form>
|
||||
{ keyInput }
|
||||
<Input
|
||||
hint={
|
||||
isExternal
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.decryptKey.hint'
|
||||
defaultMessage='decrypt the key'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.unlockAccount.hint'
|
||||
defaultMessage='unlock the account'
|
||||
/>
|
||||
)
|
||||
}
|
||||
label={
|
||||
isExternal
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.decryptKey.label'
|
||||
defaultMessage='Key Password'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.unlockAccount.label'
|
||||
defaultMessage='Account Password'
|
||||
/>
|
||||
)
|
||||
}
|
||||
onChange={ this.onModifyPassword }
|
||||
onKeyDown={ this.onKeyDown }
|
||||
ref='input'
|
||||
type='password'
|
||||
value={ password }
|
||||
/>
|
||||
<div className={ styles.passwordHint }>
|
||||
{ passwordHint }
|
||||
</div>
|
||||
{ this.renderKeyInput() }
|
||||
{ this.renderPassword() }
|
||||
{ this.renderHint() }
|
||||
<div
|
||||
data-effect='solid'
|
||||
data-for={ `transactionConfirmForm${this.id}` }
|
||||
@ -169,7 +111,7 @@ class TransactionPendingFormConfirm extends Component {
|
||||
>
|
||||
<RaisedButton
|
||||
className={ styles.confirmButton }
|
||||
disabled={ isSending || !isWalletOk }
|
||||
disabled={ disabled || isSending || !isWalletOk }
|
||||
fullWidth
|
||||
icon={
|
||||
<IdentityIcon
|
||||
@ -203,16 +145,120 @@ class TransactionPendingFormConfirm extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderPassword () {
|
||||
const { account } = this.props;
|
||||
const { password } = this.state;
|
||||
|
||||
if (account && account.hardware) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
hint={
|
||||
account.uuid
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.password.unlock.hint'
|
||||
defaultMessage='unlock the account'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.password.decrypt.hint'
|
||||
defaultMessage='decrypt the key'
|
||||
/>
|
||||
)
|
||||
}
|
||||
label={
|
||||
account.uuid
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.password.unlock.label'
|
||||
defaultMessage='Account Password'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.password.decrypt.label'
|
||||
defaultMessage='Key Password'
|
||||
/>
|
||||
)
|
||||
}
|
||||
onChange={ this.onModifyPassword }
|
||||
onKeyDown={ this.onKeyDown }
|
||||
ref='input'
|
||||
type='password'
|
||||
value={ password }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderHint () {
|
||||
const { account, disabled, isSending } = this.props;
|
||||
|
||||
if (account.hardware) {
|
||||
if (isSending) {
|
||||
return (
|
||||
<div className={ styles.passwordHint }>
|
||||
<FormattedMessage
|
||||
id='signer.sending.hardware.confirm'
|
||||
defaultMessage='Please confirm the transaction on your attached hardware device'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (disabled) {
|
||||
return (
|
||||
<div className={ styles.passwordHint }>
|
||||
<FormattedMessage
|
||||
id='signer.sending.hardware.connect'
|
||||
defaultMessage='Please attach your hardware device before confirming the transaction'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const passwordHint = this.getPasswordHint();
|
||||
|
||||
if (!passwordHint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.passwordHint }>
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.passwordHint'
|
||||
defaultMessage='(hint) {passwordHint}'
|
||||
values={ {
|
||||
passwordHint
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderKeyInput () {
|
||||
const { account } = this.props;
|
||||
const { walletError } = this.state;
|
||||
|
||||
if (account.uuid || account.wallet || account.hardware) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={ styles.fileInput }
|
||||
error={ walletError }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.selectKey.hint'
|
||||
defaultMessage='The keyfile to use for this account'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.keySelect.label'
|
||||
id='signer.txPendingConfirm.selectKey.label'
|
||||
defaultMessage='Select Local Key'
|
||||
/>
|
||||
}
|
||||
@ -223,7 +269,9 @@ class TransactionPendingFormConfirm extends Component {
|
||||
}
|
||||
|
||||
renderTooltip () {
|
||||
if (this.state.password.length) {
|
||||
const { account } = this.props;
|
||||
|
||||
if (this.state.password.length || account.hardware) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -290,7 +338,8 @@ class TransactionPendingFormConfirm extends Component {
|
||||
const { password, wallet } = this.state;
|
||||
|
||||
this.props.onConfirm({
|
||||
password, wallet
|
||||
password,
|
||||
wallet
|
||||
});
|
||||
}
|
||||
|
||||
@ -304,20 +353,3 @@ class TransactionPendingFormConfirm extends Component {
|
||||
this.onConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (_, initProps) {
|
||||
const { address } = initProps;
|
||||
|
||||
return (state) => {
|
||||
const { accounts } = state.personal;
|
||||
let gotAddress = Object.keys(accounts).find(a => a.toLowerCase() === address.toLowerCase());
|
||||
const account = gotAddress ? accounts[gotAddress] : {};
|
||||
|
||||
return { account };
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(TransactionPendingFormConfirm);
|
||||
|
@ -0,0 +1,134 @@
|
||||
// Copyright 2015-2017 Parity Technologies (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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import TransactionPendingFormConfirm from './';
|
||||
|
||||
const ADDR_NORMAL = '0x0123456789012345678901234567890123456789';
|
||||
const ADDR_WALLET = '0x1234567890123456789012345678901234567890';
|
||||
const ADDR_HARDWARE = '0x2345678901234567890123456789012345678901';
|
||||
const ADDR_SIGN = '0x3456789012345678901234567890123456789012';
|
||||
const ACCOUNTS = {
|
||||
[ADDR_NORMAL]: {
|
||||
address: ADDR_NORMAL,
|
||||
uuid: ADDR_NORMAL
|
||||
},
|
||||
[ADDR_WALLET]: {
|
||||
address: ADDR_WALLET,
|
||||
wallet: true
|
||||
},
|
||||
[ADDR_HARDWARE]: {
|
||||
address: ADDR_HARDWARE,
|
||||
hardware: true
|
||||
}
|
||||
};
|
||||
|
||||
let component;
|
||||
let instance;
|
||||
let onConfirm;
|
||||
|
||||
function render (address) {
|
||||
onConfirm = sinon.stub();
|
||||
|
||||
component = shallow(
|
||||
<TransactionPendingFormConfirm
|
||||
account={ ACCOUNTS[address] || {} }
|
||||
address={ address }
|
||||
onConfirm={ onConfirm }
|
||||
isSending={ false }
|
||||
/>
|
||||
);
|
||||
instance = component.instance();
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('views/Signer/TransactionPendingFormConfirm', () => {
|
||||
describe('normal accounts', () => {
|
||||
beforeEach(() => {
|
||||
render(ADDR_NORMAL);
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
it('does not render the key input', () => {
|
||||
expect(instance.renderKeyInput()).to.be.null;
|
||||
});
|
||||
|
||||
it('renders the password', () => {
|
||||
expect(instance.renderPassword()).not.to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('hardware accounts', () => {
|
||||
beforeEach(() => {
|
||||
render(ADDR_HARDWARE);
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
it('does not render the key input', () => {
|
||||
expect(instance.renderKeyInput()).to.be.null;
|
||||
});
|
||||
|
||||
it('does not render the password', () => {
|
||||
expect(instance.renderPassword()).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('wallet accounts', () => {
|
||||
beforeEach(() => {
|
||||
render(ADDR_WALLET);
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
it('does not render the key input', () => {
|
||||
expect(instance.renderKeyInput()).to.be.null;
|
||||
});
|
||||
|
||||
it('renders the password', () => {
|
||||
expect(instance.renderPassword()).not.to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('signing accounts', () => {
|
||||
beforeEach(() => {
|
||||
render(ADDR_SIGN);
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
it('renders the key input', () => {
|
||||
expect(instance.renderKeyInput()).not.to.be.null;
|
||||
});
|
||||
|
||||
it('renders the password', () => {
|
||||
expect(instance.renderPassword()).not.to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
@ -25,7 +25,9 @@ import styles from './transactionPendingForm.css';
|
||||
|
||||
export default class TransactionPendingForm extends Component {
|
||||
static propTypes = {
|
||||
account: PropTypes.object.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
isSending: PropTypes.bool.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onReject: PropTypes.func.isRequired,
|
||||
@ -53,7 +55,7 @@ export default class TransactionPendingForm extends Component {
|
||||
}
|
||||
|
||||
renderForm () {
|
||||
const { address, focus, isSending, onConfirm, onReject } = this.props;
|
||||
const { account, address, disabled, focus, isSending, onConfirm, onReject } = this.props;
|
||||
|
||||
if (this.state.isRejectOpen) {
|
||||
return (
|
||||
@ -64,6 +66,8 @@ export default class TransactionPendingForm extends Component {
|
||||
return (
|
||||
<TransactionPendingFormConfirm
|
||||
address={ address }
|
||||
account={ account }
|
||||
disabled={ disabled }
|
||||
focus={ focus }
|
||||
isSending={ isSending }
|
||||
onConfirm={ onConfirm }
|
||||
|
@ -97,7 +97,7 @@ class Embedded extends Component {
|
||||
onReject={ actions.startRejectRequest }
|
||||
origin={ origin }
|
||||
payload={ payload }
|
||||
store={ this.store }
|
||||
signerstore={ this.store }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ class RequestsPage extends Component {
|
||||
onReject={ actions.startRejectRequest }
|
||||
origin={ origin }
|
||||
payload={ payload }
|
||||
store={ this.store }
|
||||
signerstore={ this.store }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ export Contract from './Contract';
|
||||
export Contracts from './Contracts';
|
||||
export Dapp from './Dapp';
|
||||
export Dapps from './Dapps';
|
||||
export HistoryStore from './historyStore';
|
||||
export Home from './Home';
|
||||
export ParityBar from './ParityBar';
|
||||
export Settings, { SettingsBackground, SettingsParity, SettingsProxy, SettingsViews } from './Settings';
|
||||
|
@ -43,6 +43,7 @@ global.WebSocket = WebSocket;
|
||||
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
|
||||
global.window = document.defaultView;
|
||||
global.navigator = global.window.navigator;
|
||||
global.location = global.window.location;
|
||||
|
||||
// attach mocked localStorage onto the window as exposed by jsdom
|
||||
global.window.localStorage = global.localStorage;
|
||||
|
Loading…
Reference in New Issue
Block a user