Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Commit

Permalink
UI support for hardware wallets (#4539)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
jacogr authored and gavofyork committed Mar 2, 2017
1 parent 96d7454 commit b11caaf
Show file tree
Hide file tree
Showing 44 changed files with 1,650 additions and 260 deletions.
4 changes: 3 additions & 1 deletion js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 1 addition & 9 deletions js/src/3rdparty/ledger/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 136 additions & 0 deletions js/src/3rdparty/ledger/ledger.js
Original file line number Diff line number Diff line change
@@ -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 changes: 120 additions & 0 deletions js/src/3rdparty/ledger/ledger.spec.js
Original file line number Diff line number Diff line change
@@ -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;
});
});
});
12 changes: 12 additions & 0 deletions js/src/api/format/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
12 changes: 11 additions & 1 deletion js/src/api/format/output.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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');
Expand Down
8 changes: 7 additions & 1 deletion js/src/api/rpc/parity/parity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
26 changes: 26 additions & 0 deletions js/src/jsonrpc/interfaces/parity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
Loading

0 comments on commit b11caaf

Please sign in to comment.