Update v1 Wallet Dapp (#6935)

* Start removing duplicated functionality (v1 inside v2)

* Update compilation targets

* Update locks

* Fix js-old build

* Update with removed extra references

* Adapt dev.{parity,web3}.html for extra debug info

* Update dependencies

* Remove Tooltips

* Update dependencies

* Only inject window.ethereum once

* Fix versions to 2.0.x for @parity libraries

* Update to @parity/api 2.1.x

* Update for @parity/api 2.1.x

* Freeze signer plugin dependency hashes

* Fix lint

* Move local account handling from API

* Update for 2.2.x @parity/{shared,ui}

* Update API references for middleware

* Install updated dependencies

* Update for build

* Always do local builds for development

* Remove unused hasAccounts property

* Fix Windows build for js-old

* Adjust inclusing rules to be Windows friendly

* Explicitly add --config option to webpack

* Add process.env.EMBED flag for Windows compatability

* Revert embed flag

* Fix build

* Merge changes from beta

* Update packages after merge

* Update Accounts from beta

* Update with beta

* Remove upgrade check

* Fix CI build script execution

* Make rm -rf commands cross-platform

* Remove ability to deploy wallets (only watch)

* Update path references for js-old (Windows)

* Render local dapps first

* Cleanup dependencies
This commit is contained in:
Jaco Greeff
2017-11-13 09:31:08 +01:00
committed by GitHub
parent bcdfc50a0b
commit ce1609726f
97 changed files with 8062 additions and 14290 deletions

View File

@@ -0,0 +1,118 @@
// 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/>.
const { createKeyObject, decryptPrivateKey } = require('../ethkey');
class Account {
constructor (persist, data = {}) {
const {
keyObject = null,
meta = {},
name = ''
} = data;
this._persist = persist;
this._keyObject = keyObject;
this._name = name;
this._meta = meta;
}
isValidPassword (password) {
if (!this._keyObject) {
return false;
}
return decryptPrivateKey(this._keyObject, password)
.then((privateKey) => {
if (!privateKey) {
return false;
}
return true;
});
}
export () {
const exported = Object.assign({}, this._keyObject);
exported.meta = JSON.stringify(this._meta);
exported.name = this._name;
return exported;
}
get address () {
return `0x${this._keyObject.address.toLowerCase()}`;
}
get name () {
return this._name;
}
set name (name) {
this._name = name;
this._persist();
}
get meta () {
return JSON.stringify(this._meta);
}
set meta (meta) {
this._meta = JSON.parse(meta);
this._persist();
}
get uuid () {
if (!this._keyObject) {
return null;
}
return this._keyObject.id;
}
decryptPrivateKey (password) {
return decryptPrivateKey(this._keyObject, password);
}
changePassword (key, password) {
return createKeyObject(key, password).then((keyObject) => {
this._keyObject = keyObject;
this._persist();
});
}
toJSON () {
return {
keyObject: this._keyObject,
name: this._name,
meta: this._meta
};
}
}
Account.fromPrivateKey = function (persist, key, password) {
return createKeyObject(key, password).then((keyObject) => {
const account = new Account(persist, { keyObject });
return account;
});
};
module.exports = Account;

View File

@@ -0,0 +1,238 @@
// 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/>.
const EventEmitter = require('eventemitter3');
const { debounce } = require('lodash');
const localStore = require('store');
const Account = require('./account');
const { decryptPrivateKey } = require('../ethkey');
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
const LS_STORE_KEY = '_parity::localAccounts';
class Accounts extends EventEmitter {
constructor (data = localStore.get(LS_STORE_KEY) || {}) {
super();
this.persist = debounce(() => {
this._lastState = JSON.stringify(this);
localStore.set(LS_STORE_KEY, this);
}, 100);
this._addAccount = this._addAccount.bind(this);
this._lastState = JSON.stringify(data);
window.addEventListener('storage', ({ key, newValue }) => {
if (key !== LS_STORE_KEY) {
return;
}
if (newValue !== this._lastState) {
console.log('Data changed in a second tab, syncing state');
this.restore(JSON.parse(newValue));
}
});
this.restore(data);
}
restore (data) {
const {
last = NULL_ADDRESS,
dappsDefault = NULL_ADDRESS,
store = {}
} = data;
this._last = last;
this._dappsDefaultAddress = dappsDefault;
this._store = {};
if (Array.isArray(store)) {
// Recover older version that stored accounts as an array
store.forEach((data) => {
const account = new Account(this.persist, data);
this._store[account.address] = account;
});
} else {
Object.keys(store).forEach((key) => {
this._store[key] = new Account(this.persist, store[key]);
});
}
}
_addAccount (account) {
const { address } = account;
if (address in this._store && this._store[address].uuid) {
throw new Error(`Account ${address} already exists!`);
}
this._store[address] = account;
this.lastAddress = address;
this.persist();
return account.address;
}
create (secret, password) {
const privateKey = Buffer.from(secret.slice(2), 'hex');
return Account
.fromPrivateKey(this.persist, privateKey, password)
.then(this._addAccount);
}
restoreFromWallet (wallet, password) {
return decryptPrivateKey(wallet, password)
.then((privateKey) => {
if (!privateKey) {
throw new Error('Invalid password');
}
return Account.fromPrivateKey(this.persist, privateKey, password);
})
.then(this._addAccount);
}
set lastAddress (value) {
this._last = value.toLowerCase();
}
get lastAddress () {
return this._last;
}
get dappsDefaultAddress () {
if (this._dappsDefaultAddress === NULL_ADDRESS) {
return this._last;
}
if (this._dappsDefaultAddress in this._store) {
return this._dappsDefaultAddress;
}
return NULL_ADDRESS;
}
set dappsDefaultAddress (value) {
this._dappsDefaultAddress = value.toLowerCase();
this.emit('dappsDefaultAddressChange', this._dappsDefaultAddress);
this.persist();
}
get (address) {
address = address.toLowerCase();
const account = this._store[address];
if (!account) {
throw new Error(`Account not found: ${address}`);
}
this.lastAddress = address;
return account;
}
getLazyCreate (address) {
address = address.toLowerCase();
this.lastAddress = address;
if (!(address in this._store)) {
this._store[address] = new Account(this.persist);
}
return this._store[address];
}
remove (address, password) {
address = address.toLowerCase();
const account = this.get(address);
if (!account) {
return false;
}
if (!account.uuid) {
this.removeUnsafe(address);
return true;
}
return account
.isValidPassword(password)
.then((isValid) => {
if (!isValid) {
return false;
}
if (address === this.lastAddress) {
this.lastAddress = NULL_ADDRESS;
}
this.removeUnsafe(address);
return true;
});
}
removeUnsafe (address) {
address = address.toLowerCase();
delete this._store[address];
this.persist();
}
allAddresses () {
return Object.keys(this._store);
}
accountAddresses () {
return Object
.keys(this._store)
.filter((address) => this._store[address].uuid);
}
map (mapper) {
const result = {};
Object.keys(this._store).forEach((key) => {
result[key] = mapper(this._store[key]);
});
return result;
}
toJSON () {
return {
last: this._last,
dappsDefault: this._dappsDefaultAddress,
store: this._store
};
}
}
module.exports = Accounts;

View File

@@ -0,0 +1,21 @@
// 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/>.
const Accounts = require('./accounts');
const accounts = new Accounts();
module.exports = accounts;

View File

@@ -0,0 +1,160 @@
// 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/>.
/* global WebAssembly */
const wasmBuffer = require('./ethkey.wasm.js');
const NOOP = () => {};
// WASM memory setup
const WASM_PAGE_SIZE = 65536;
const STATIC_BASE = 1024;
const STATICTOP = STATIC_BASE + WASM_PAGE_SIZE * 2;
const STACK_BASE = align(STATICTOP + 16);
const STACKTOP = STACK_BASE;
const TOTAL_STACK = 5 * 1024 * 1024;
const TOTAL_MEMORY = 16777216;
const STACK_MAX = STACK_BASE + TOTAL_STACK;
const DYNAMIC_BASE = STACK_MAX + 64;
const DYNAMICTOP_PTR = STACK_MAX;
function mockWebAssembly () {
function throwWasmError () {
throw new Error('Missing WebAssembly support');
}
// Simple mock replacement
return {
Memory: class {
constructor () {
this.buffer = new ArrayBuffer(2048);
}
},
Table: class {
},
Module: class {
},
Instance: class {
constructor () {
this.exports = {
'_input_ptr': () => 0,
'_secret_ptr': () => 0,
'_public_ptr': () => 0,
'_address_ptr': () => 0,
'_ecpointg': NOOP,
'_brain': throwWasmError,
'_verify_secret': throwWasmError
};
}
}
};
}
const { Memory, Table, Module, Instance } = typeof WebAssembly !== 'undefined' ? WebAssembly : mockWebAssembly();
const wasmMemory = new Memory({
initial: TOTAL_MEMORY / WASM_PAGE_SIZE,
maximum: TOTAL_MEMORY / WASM_PAGE_SIZE
});
const wasmTable = new Table({
initial: 8,
maximum: 8,
element: 'anyfunc'
});
// TypedArray views into the memory
const wasmMemoryU8 = new Uint8Array(wasmMemory.buffer);
const wasmMemoryU32 = new Uint32Array(wasmMemory.buffer);
// Keep DYNAMIC_BASE in memory
wasmMemoryU32[DYNAMICTOP_PTR >> 2] = align(DYNAMIC_BASE);
function align (mem) {
const ALIGN_SIZE = 16;
return (Math.ceil(mem / ALIGN_SIZE) * ALIGN_SIZE) | 0;
}
function slice (ptr, len) {
return wasmMemoryU8.subarray(ptr, ptr + len);
}
// Required by emscripten
function abort (what) {
throw new Error(what || 'WASM abort');
}
// Required by emscripten
function abortOnCannotGrowMemory () {
abort(`Cannot enlarge memory arrays.`);
}
// Required by emscripten
function enlargeMemory () {
abortOnCannotGrowMemory();
}
// Required by emscripten
function getTotalMemory () {
return TOTAL_MEMORY;
}
// Required by emscripten - used to perform memcpy on large data
function memcpy (dest, src, len) {
wasmMemoryU8.set(wasmMemoryU8.subarray(src, src + len), dest);
return dest;
}
// Synchronously compile WASM from the buffer
const wasmModule = new Module(wasmBuffer);
// Instantiated WASM module
const instance = new Instance(wasmModule, {
global: {},
env: {
DYNAMICTOP_PTR,
STACKTOP,
STACK_MAX,
abort,
enlargeMemory,
getTotalMemory,
abortOnCannotGrowMemory,
___lock: NOOP,
___syscall6: () => 0,
___setErrNo: (no) => no,
_abort: abort,
___syscall140: () => 0,
_emscripten_memcpy_big: memcpy,
___syscall54: () => 0,
___unlock: NOOP,
_llvm_trap: abort,
___syscall146: () => 0,
'memory': wasmMemory,
'table': wasmTable,
tableBase: 0,
memoryBase: STATIC_BASE
}
});
const extern = instance.exports;
module.exports = {
extern,
slice
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,55 @@
// 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/>.
const workerPool = require('./workerPool');
function createKeyObject (key, password) {
return workerPool.action('createKeyObject', { key, password })
.then((obj) => JSON.parse(obj));
}
function decryptPrivateKey (keyObject, password) {
return workerPool
.action('decryptPrivateKey', { keyObject, password })
.then((privateKey) => {
if (privateKey) {
return Buffer.from(privateKey);
}
return null;
});
}
function phraseToAddress (phrase) {
return phraseToWallet(phrase)
.then((wallet) => wallet.address);
}
function phraseToWallet (phrase) {
return workerPool.action('phraseToWallet', phrase);
}
function verifySecret (secret) {
return workerPool.action('verifySecret', secret);
}
module.exports = {
createKeyObject,
decryptPrivateKey,
phraseToAddress,
phraseToWallet,
verifySecret
};

View File

@@ -0,0 +1,59 @@
// 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/>.
const { randomPhrase } = require('@parity/wordlist');
const { phraseToAddress, phraseToWallet } = require('./index');
// TODO: Skipping until Node.js 8.0 comes out and we can test WebAssembly
describe.skip('api/local/ethkey', () => {
describe('phraseToAddress', function () {
this.timeout(30000);
it('generates a valid address', () => {
const phrase = randomPhrase(12);
return phraseToAddress(phrase).then((address) => {
expect(address.length).to.be.equal(42);
expect(address.slice(0, 4)).to.be.equal('0x00');
});
});
it('generates valid address for empty phrase', () => {
return phraseToAddress('').then((address) => {
expect(address).to.be.equal('0x00a329c0648769a73afac7f9381e08fb43dbea72');
});
});
});
describe('phraseToWallet', function () {
this.timeout(30000);
it('generates a valid wallet object', () => {
const phrase = randomPhrase(12);
return phraseToWallet(phrase).then((wallet) => {
expect(wallet.address.length).to.be.equal(42);
expect(wallet.secret.length).to.be.equal(66);
expect(wallet.public.length).to.be.equal(130);
expect(wallet.address.slice(0, 4)).to.be.equal('0x00');
expect(wallet.secret.slice(0, 2)).to.be.equal('0x');
expect(wallet.public.slice(0, 2)).to.be.equal('0x');
});
});
});
});

View File

@@ -0,0 +1,139 @@
// 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/>.
const { bytesToHex } = require('@parity/api/lib/util/format');
const { extern, slice } = require('./ethkey.js');
const isWorker = typeof self !== 'undefined';
// Stay compatible between environments
if (!isWorker) {
const scope = typeof global === 'undefined' ? window : global;
scope.self = scope;
}
// keythereum should never be used outside of the browser
let keythereum = require('keythereum');
if (isWorker) {
keythereum = self.keythereum;
}
function route ({ action, payload }) {
if (action in actions) {
return actions[action](payload);
}
return null;
}
const input = slice(extern._input_ptr(), 1024);
const secret = slice(extern._secret_ptr(), 32);
const publicKey = slice(extern._public_ptr(), 64);
const address = slice(extern._address_ptr(), 20);
extern._ecpointg();
const actions = {
phraseToWallet (phrase) {
const phraseUtf8 = Buffer.from(phrase, 'utf8');
if (phraseUtf8.length > input.length) {
throw new Error('Phrase is too long!');
}
input.set(phraseUtf8);
extern._brain(phraseUtf8.length);
const wallet = {
secret: bytesToHex(secret),
public: bytesToHex(publicKey),
address: bytesToHex(address)
};
return wallet;
},
verifySecret (key) {
const keyBuf = Buffer.from(key.slice(2), 'hex');
secret.set(keyBuf);
return extern._verify_secret();
},
createKeyObject ({ key, password }) {
key = Buffer.from(key);
password = Buffer.from(password);
const iv = keythereum.crypto.randomBytes(16);
const salt = keythereum.crypto.randomBytes(32);
const keyObject = keythereum.dump(password, key, salt, iv);
return JSON.stringify(keyObject);
},
decryptPrivateKey ({ keyObject, password }) {
password = Buffer.from(password);
try {
const key = keythereum.recover(password, keyObject);
// Convert to array to safely send from the worker
return Array.from(key);
} catch (e) {
return null;
}
}
};
self.onmessage = function ({ data }) {
try {
const result = route(data);
postMessage([null, result]);
} catch (err) {
console.error(err);
postMessage([err.toString(), null]);
}
};
// Emulate a web worker in Node.js
class KeyWorker {
postMessage (data) {
// Force async
setTimeout(() => {
try {
const result = route(data);
this.onmessage({ data: [null, result] });
} catch (err) {
this.onmessage({ data: [err, null] });
}
}, 0);
}
onmessage (event) {
// no-op to be overriden
}
}
if (exports != null) {
exports.KeyWorker = KeyWorker;
}

View File

@@ -0,0 +1,110 @@
// 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/>.
// Allow a web worker in the browser, with a fallback for Node.js
const hasWebWorkers = typeof Worker !== 'undefined';
const KeyWorker = hasWebWorkers
? require('worker-loader!./worker') // eslint-disable-line import/no-webpack-loader-syntax
: require('./worker').KeyWorker;
class WorkerContainer {
constructor () {
this.busy = false;
this._worker = new KeyWorker();
}
action (action, payload) {
if (this.busy) {
throw new Error('Cannot issue an action on a busy worker!');
}
this.busy = true;
return new Promise((resolve, reject) => {
this._worker.postMessage({ action, payload });
this._worker.onmessage = ({ data }) => {
const [err, result] = data;
this.busy = false;
if (err) {
// `err` ought to be a String
reject(new Error(err));
} else {
resolve(result);
}
};
});
}
}
class WorkerPool {
constructor () {
this.pool = [
new WorkerContainer(),
new WorkerContainer()
];
this.queue = [];
}
_getContainer () {
return this.pool.find((container) => !container.busy);
}
action (action, payload) {
let container = this.pool.find((container) => !container.busy);
let promise;
// const start = Date.now();
if (container) {
promise = container.action(action, payload);
} else {
promise = new Promise((resolve, reject) => {
this.queue.push([action, payload, resolve]);
});
}
return promise
.catch((err) => {
this.processQueue();
throw err;
})
.then((result) => {
this.processQueue();
// console.log('Work done in ', Date.now() - start);
return result;
});
}
processQueue () {
let container = this._getContainer();
while (container && this.queue.length > 0) {
const [action, payload, resolve] = this.queue.shift();
resolve(container.action(action, payload));
container = this._getContainer();
}
}
}
module.exports = new WorkerPool();

View File

@@ -0,0 +1,309 @@
// 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/>.
const EthereumTx = require('ethereumjs-tx');
const { Middleware } = require('@parity/api/lib/transport');
const { inNumber16 } = require('@parity/api/lib/format/input');
const { randomPhrase } = require('@parity/wordlist');
const accounts = require('./accounts');
const transactions = require('./transactions');
const { phraseToWallet, phraseToAddress, verifySecret } = require('./ethkey');
class LocalAccountsMiddleware extends Middleware {
constructor (transport) {
super(transport);
const NOOP = () => {};
const register = this.register.bind(this);
const registerSubscribe = this.registerSubscribe.bind(this);
register('eth_accounts', () => {
return accounts.accountAddresses();
});
register('eth_coinbase', () => {
return accounts.lastAddress;
});
register('parity_accountsInfo', () => {
return accounts.map(({ name }) => {
return { name };
});
});
register('parity_allAccountsInfo', () => {
return accounts.map(({ name, meta, uuid }) => {
return { name, meta, uuid };
});
});
register('parity_changePassword', ([address, oldPassword, newPassword]) => {
const account = accounts.get(address);
return account
.decryptPrivateKey(oldPassword)
.then((privateKey) => {
if (!privateKey) {
return false;
}
account.changePassword(privateKey, newPassword);
return true;
});
});
register('parity_checkRequest', ([id]) => {
return transactions.hash(id) || Promise.resolve(null);
});
register('parity_dappsList', () => {
return [];
});
register('parity_defaultAccount', () => {
return accounts.dappsDefaultAddress;
});
registerSubscribe('parity_defaultAccount', (_, callback) => {
callback(null, accounts.dappsDefaultAddress);
accounts.on('dappsDefaultAddressChange', (address) => {
callback(null, accounts.dappsDefaultAddress);
});
});
register('parity_exportAccount', ([address, password]) => {
const account = accounts.get(address);
if (!password) {
password = '';
}
return account.isValidPassword(password)
.then((isValid) => {
if (!isValid) {
throw new Error('Invalid password');
}
return account.export();
});
});
register('parity_generateSecretPhrase', () => {
return randomPhrase(12);
});
register('parity_getNewDappsAddresses', () => {
return accounts.accountAddresses();
});
register('parity_getNewDappsDefaultAddress', () => {
return accounts.dappsDefaultAddress;
});
register('parity_hardwareAccountsInfo', () => {
return {};
});
registerSubscribe('parity_hardwareAccountsInfo', NOOP);
register('parity_newAccountFromPhrase', ([phrase, password]) => {
return phraseToWallet(phrase)
.then((wallet) => {
return accounts.create(wallet.secret, password);
});
});
register('parity_newAccountFromSecret', ([secret, password]) => {
return verifySecret(secret)
.then((isValid) => {
if (!isValid) {
throw new Error('Invalid secret key');
}
return accounts.create(secret, password);
});
});
register('parity_newAccountFromWallet', ([json, password]) => {
if (!password) {
password = '';
}
return accounts.restoreFromWallet(JSON.parse(json), password);
});
register('parity_setAccountMeta', ([address, meta]) => {
accounts.getLazyCreate(address).meta = meta;
return true;
});
register('parity_setAccountName', ([address, name]) => {
accounts.getLazyCreate(address).name = name;
return true;
});
register('parity_setNewDappsDefaultAddress', ([address]) => {
accounts.dappsDefaultAddress = address;
return true;
});
register('parity_postTransaction', ([tx]) => {
if (!tx.from) {
tx.from = accounts.lastAddress;
}
tx.nonce = null;
tx.condition = null;
return transactions.add(tx);
});
register('parity_phraseToAddress', ([phrase]) => {
return phraseToAddress(phrase);
});
register('parity_useLocalAccounts', () => {
return true;
});
register('parity_listGethAccounts', () => {
return [];
});
register('parity_listOpenedVaults', () => {
return [];
});
register('parity_listRecentDapps', () => {
return {};
});
register('parity_listVaults', () => {
return [];
});
register('parity_lockedHardwareAccountsInfo', () => {
return [];
});
register('parity_hashContent', () => {
throw new Error('Functionality unavailable on a public wallet.');
});
register('parity_killAccount', ([address, password]) => {
return accounts.remove(address, password);
});
register('parity_removeAddress', ([address]) => {
return accounts.remove(address, null);
});
register('parity_testPassword', ([address, password]) => {
const account = accounts.get(address);
return account.isValidPassword(password);
});
register('parity_upgradeReady', () => {
return false;
});
register('signer_confirmRequest', ([id, modify, password]) => {
const {
gasPrice,
gas: gasLimit,
from,
to,
value,
data
} = Object.assign(transactions.get(id), modify);
transactions.lock(id);
const account = accounts.get(from);
return Promise
.all([
this.rpcRequest('parity_nextNonce', [from]),
account.decryptPrivateKey(password)
])
.catch((err) => {
transactions.unlock(id);
// transaction got unlocked, can propagate rejection further
throw err;
})
.then(([nonce, privateKey]) => {
if (!privateKey) {
transactions.unlock(id);
throw new Error('Invalid password');
}
const tx = new EthereumTx({
nonce,
to,
data,
gasLimit: inNumber16(gasLimit),
gasPrice: inNumber16(gasPrice),
value: inNumber16(value)
});
tx.sign(privateKey);
const serializedTx = `0x${tx.serialize().toString('hex')}`;
return this.rpcRequest('eth_sendRawTransaction', [serializedTx]);
})
.then((hash) => {
transactions.confirm(id, hash);
return {};
});
});
register('signer_generateAuthorizationToken', () => {
return '';
});
register('signer_rejectRequest', ([id]) => {
return transactions.reject(id);
});
register('signer_requestsToConfirm', () => {
return transactions.requestsToConfirm();
});
registerSubscribe('signer_subscribePending', (_, callback) => {
callback(null, transactions.requestsToConfirm());
transactions.on('update', () => {
callback(null, transactions.requestsToConfirm());
});
return false;
});
}
}
module.exports = LocalAccountsMiddleware;

View File

@@ -0,0 +1,160 @@
// 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/>.
/* eslint-disable no-unused-expressions */
const JsonRpcBase = require('@parity/api/lib/transport/jsonRpcBase');
const LocalAccountsMiddleware = require('./localAccountsMiddleware');
const RPC_RESPONSE = Symbol('RPC response');
const ADDRESS = '0x00a329c0648769a73afac7f9381e08fb43dbea72';
const SECRET = '0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7';
const PASSWORD = 'password';
const FOO_PHRASE = 'foobar';
const FOO_PASSWORD = 'foopass';
const FOO_ADDRESS = '0x007ef7ac1058e5955e366ab9d6b6c4ebcc937e7e';
class MockedTransport extends JsonRpcBase {
_execute (method, params) {
return RPC_RESPONSE;
}
}
// Skip till all CI runs on Node 8+
describe.skip('api/local/LocalAccountsMiddleware', function () {
this.timeout(30000);
let transport;
beforeEach(() => {
transport = new MockedTransport();
transport.addMiddleware(LocalAccountsMiddleware);
// Same as `parity_newAccountFromPhrase` with empty phrase
return transport
.execute('parity_newAccountFromSecret', [SECRET, PASSWORD])
.catch((_err) => {
// Ignore the error - all instances of LocalAccountsMiddleware
// share account storage
});
});
it('registers all necessary methods', () => {
return Promise
.all([
'eth_accounts',
'eth_coinbase',
'parity_accountsInfo',
'parity_allAccountsInfo',
'parity_changePassword',
'parity_checkRequest',
'parity_defaultAccount',
'parity_generateSecretPhrase',
'parity_getNewDappsAddresses',
'parity_hardwareAccountsInfo',
'parity_newAccountFromPhrase',
'parity_newAccountFromSecret',
'parity_setAccountMeta',
'parity_setAccountName',
'parity_postTransaction',
'parity_phraseToAddress',
'parity_useLocalAccounts',
'parity_listGethAccounts',
'parity_listOpenedVaults',
'parity_listRecentDapps',
'parity_listVaults',
'parity_killAccount',
'parity_testPassword',
'signer_confirmRequest',
'signer_rejectRequest',
'signer_requestsToConfirm'
].map((method) => {
return transport
.execute(method)
.then((result) => {
expect(result).not.to.be.equal(RPC_RESPONSE);
})
// Some errors are expected here since we are calling methods
// without parameters.
.catch((_) => {});
}));
});
it('allows non-registered methods through', () => {
return transport
.execute('eth_getBalance', ['0x407d73d8a49eeb85d32cf465507dd71d507100c1'])
.then((result) => {
expect(result).to.be.equal(RPC_RESPONSE);
});
});
it('can handle `eth_accounts`', () => {
return transport
.execute('eth_accounts')
.then((accounts) => {
expect(accounts.length).to.be.equal(1);
expect(accounts[0]).to.be.equal(ADDRESS);
});
});
it('can handle `parity_defaultAccount`', () => {
return transport
.execute('parity_defaultAccount')
.then((address) => {
expect(address).to.be.equal(ADDRESS);
});
});
it('can handle `parity_phraseToAddress`', () => {
return transport
.execute('parity_phraseToAddress', [''])
.then((address) => {
expect(address).to.be.equal(ADDRESS);
return transport.execute('parity_phraseToAddress', [FOO_PHRASE]);
})
.then((address) => {
expect(address).to.be.equal(FOO_ADDRESS);
});
});
it('can create and kill an account', () => {
return transport
.execute('parity_newAccountFromPhrase', [FOO_PHRASE, FOO_PASSWORD])
.then((address) => {
expect(address).to.be.equal(FOO_ADDRESS);
return transport.execute('eth_accounts');
})
.then((accounts) => {
expect(accounts.length).to.be.equal(2);
expect(accounts.includes(FOO_ADDRESS)).to.be.true;
return transport.execute('parity_killAccount', [FOO_ADDRESS, FOO_PASSWORD]);
})
.then((result) => {
expect(result).to.be.true;
return transport.execute('eth_accounts');
})
.then((accounts) => {
expect(accounts.length).to.be.equal(1);
expect(accounts.includes(FOO_ADDRESS)).to.be.false;
});
});
});

View File

@@ -0,0 +1,161 @@
// 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/>.
const EventEmitter = require('eventemitter3');
const { toHex } = require('@parity/api/lib/util/format');
const { TransportError } = require('@parity/api/lib/transport');
const AWAITING = Symbol('awaiting');
const LOCKED = Symbol('locked');
const CONFIRMED = Symbol('confirmed');
const REJECTED = Symbol('rejected');
class Transactions extends EventEmitter {
constructor () {
super();
this.reset();
}
// should only really be needed in the constructor and tests
reset () {
this._id = 1;
this._states = {};
}
nextId () {
return toHex(this._id++);
}
add (tx) {
const id = this.nextId();
this._states[id] = {
status: AWAITING,
transaction: tx
};
this.emit('update');
return id;
}
get (id) {
const state = this._states[id];
if (!state || state.status !== AWAITING) {
return null;
}
return state.transaction;
}
lock (id) {
const state = this._states[id];
if (!state || state.status !== AWAITING) {
throw new Error('Trying to lock an invalid transaction');
}
state.status = LOCKED;
this.emit('update');
}
unlock (id) {
const state = this._states[id];
if (!state || state.status !== LOCKED) {
throw new Error('Trying to unlock an invalid transaction');
}
state.status = AWAITING;
this.emit('update');
}
hash (id) {
const state = this._states[id];
if (!state) {
return null;
}
switch (state.status) {
case REJECTED:
throw TransportError.requestRejected();
case CONFIRMED:
return state.hash;
default:
return null;
}
}
confirm (id, hash) {
const state = this._states[id];
const status = state ? state.status : null;
switch (status) {
case AWAITING: break;
case LOCKED: break;
default: throw new Error('Trying to confirm an invalid transaction');
}
state.hash = hash;
state.status = CONFIRMED;
this.emit('update');
}
reject (id) {
const state = this._states[id];
if (!state) {
return false;
}
state.status = REJECTED;
this.emit('update');
return true;
}
requestsToConfirm () {
const result = [];
Object.keys(this._states).forEach((id) => {
const state = this._states[id];
if (state.status === AWAITING) {
result.push({
id,
origin: {
signer: '0x0'
},
payload: {
sendTransaction: state.transaction
}
});
}
});
return result;
}
}
module.exports = new Transactions();

View File

@@ -0,0 +1,88 @@
// 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/>.
/* eslint-disable no-unused-expressions */
const { TransportError } = require('@parity/api/lib/transport/error');
const transactions = require('./transactions');
const DUMMY_TX = 'dummy';
describe('api/local/transactions', () => {
beforeEach(() => {
transactions.reset();
});
it('can store transactions', () => {
const id1 = transactions.add(DUMMY_TX);
const id2 = transactions.add(DUMMY_TX);
const requests = transactions.requestsToConfirm();
expect(id1).to.be.equal('0x1');
expect(id2).to.be.equal('0x2');
expect(requests.length).to.be.equal(2);
expect(requests[0].id).to.be.equal(id1);
expect(requests[1].id).to.be.equal(id2);
expect(requests[0].payload.sendTransaction).to.be.equal(DUMMY_TX);
expect(requests[1].payload.sendTransaction).to.be.equal(DUMMY_TX);
});
it('can confirm transactions', () => {
const id1 = transactions.add(DUMMY_TX);
const id2 = transactions.add(DUMMY_TX);
const hash1 = '0x1111111111111111111111111111111111111111';
const hash2 = '0x2222222222222222222222222222222222222222';
transactions.confirm(id1, hash1);
transactions.confirm(id2, hash2);
const requests = transactions.requestsToConfirm();
expect(requests.length).to.be.equal(0);
expect(transactions.hash(id1)).to.be.equal(hash1);
expect(transactions.hash(id2)).to.be.equal(hash2);
});
it('can reject transactions', () => {
const id = transactions.add(DUMMY_TX);
transactions.reject(id);
const requests = transactions.requestsToConfirm();
expect(requests.length).to.be.equal(0);
expect(() => transactions.hash(id)).to.throw(TransportError);
});
it('can lock and confirm transactions', () => {
const id = transactions.add(DUMMY_TX);
const hash = '0x1111111111111111111111111111111111111111';
transactions.lock(id);
const requests = transactions.requestsToConfirm();
expect(requests.length).to.be.equal(0);
expect(transactions.get(id)).to.be.null;
expect(transactions.hash(id)).to.be.null;
transactions.confirm(id, hash);
expect(transactions.hash(id)).to.be.equal(hash);
});
});