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:
118
js/src/api-local/accounts/account.js
Normal file
118
js/src/api-local/accounts/account.js
Normal 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;
|
||||
238
js/src/api-local/accounts/accounts.js
Normal file
238
js/src/api-local/accounts/accounts.js
Normal 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;
|
||||
21
js/src/api-local/accounts/index.js
Normal file
21
js/src/api-local/accounts/index.js
Normal 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;
|
||||
160
js/src/api-local/ethkey/ethkey.js
Normal file
160
js/src/api-local/ethkey/ethkey.js
Normal 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
|
||||
};
|
||||
1
js/src/api-local/ethkey/ethkey.wasm.js
Normal file
1
js/src/api-local/ethkey/ethkey.wasm.js
Normal file
File diff suppressed because one or more lines are too long
55
js/src/api-local/ethkey/index.js
Normal file
55
js/src/api-local/ethkey/index.js
Normal 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
|
||||
};
|
||||
59
js/src/api-local/ethkey/index.spec.js
Normal file
59
js/src/api-local/ethkey/index.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
139
js/src/api-local/ethkey/worker.js
Normal file
139
js/src/api-local/ethkey/worker.js
Normal 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;
|
||||
}
|
||||
110
js/src/api-local/ethkey/workerPool.js
Normal file
110
js/src/api-local/ethkey/workerPool.js
Normal 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();
|
||||
309
js/src/api-local/localAccountsMiddleware.js
Normal file
309
js/src/api-local/localAccountsMiddleware.js
Normal 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;
|
||||
160
js/src/api-local/localAccountsMiddleware.spec.js
Normal file
160
js/src/api-local/localAccountsMiddleware.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
161
js/src/api-local/transactions.js
Normal file
161
js/src/api-local/transactions.js
Normal 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();
|
||||
88
js/src/api-local/transactions.spec.js
Normal file
88
js/src/api-local/transactions.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user