From f8c5b903209fd5d0dd991a2455604a98b1b30b48 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 10 Feb 2016 17:44:46 -0800 Subject: [PATCH] idmgmt - refactor --- app/scripts/background.js | 134 ++++++++-------- app/scripts/lib/idStore.js | 263 ++++++++++++++++++++++++++++++ app/scripts/lib/idmgmt.js | 318 ------------------------------------- package.json | 1 + 4 files changed, 333 insertions(+), 383 deletions(-) create mode 100644 app/scripts/lib/idStore.js delete mode 100644 app/scripts/lib/idmgmt.js diff --git a/app/scripts/background.js b/app/scripts/background.js index 0276124cc..4dc92cea5 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1,27 +1,18 @@ const Dnode = require('dnode') +const eos = require('end-of-stream') +const extend = require('xtend') +const EthStore = require('eth-store') const PortStream = require('./lib/port-stream.js') const MetaMaskProvider = require('./lib/metamask-provider') -const IdentityManager = require('./lib/idmgmt') -const eos = require('end-of-stream') +// const IdentityManager = require('./lib/idmgmt') +const IdentityStore = require('./lib/idStore') console.log('ready to roll') -var wallet = new IdentityManager() +// +// connect to other contexts +// -// setup provider -var zeroClient = MetaMaskProvider({ - rpcUrl: 'https://rawtestrpc.metamask.io/', - getAccounts: wallet.getAccounts.bind(wallet), - signTransaction: wallet.addUnconfirmedTransaction.bind(wallet), -}) - -wallet.setProvider(zeroClient) -zeroClient.on('block', function(block){ - wallet.newBlock(block) -}) - - -// setup messaging chrome.runtime.onConnect.addListener(connectRemote) function connectRemote(remotePort){ var isMetaMaskInternalProcess = (remotePort.name === 'popup') @@ -34,42 +25,33 @@ function connectRemote(remotePort){ } } -function handleInternalCommunication(remotePort){ - var duplex = new PortStream(remotePort) - var connection = Dnode({ - // this is annoying, have to decompose wallet - getState: wallet.getState.bind(wallet), - submitPassword: wallet.submitPassword.bind(wallet), - setSelectedAddress: wallet.setSelectedAddress.bind(wallet), - signTransaction: wallet.signTransaction.bind(wallet), - setLocked: wallet.setLocked.bind(wallet), - getAccounts: wallet.getAccounts.bind(wallet), - newBlock: wallet.newBlock.bind(wallet), - setProvider: wallet.setProvider.bind(wallet), - }) - duplex.pipe(connection).pipe(duplex) - connection.on('remote', function(remote){ - - // push updates to popup - wallet.on('update', sendUpdate) - eos(duplex, function unsubscribe(){ - wallet.removeListener('update', sendUpdate) - }) - function sendUpdate(state){ - remote.sendUpdate(state) - } - - }) - - - - // sub to metamask store -} - function handleExternalCommunication(remotePort){ remotePort.onMessage.addListener(onRpcRequest.bind(null, remotePort)) } +// +// state and network +// + +var idStore = new IdentityStore() +var zeroClient = MetaMaskProvider({ + rpcUrl: 'https://rawtestrpc.metamask.io/', + getAccounts: function(cb){ + var selectedAddress = idStore.getSelectedAddress() + var result = selectedAddress ? [selectedAddress] : [] + cb(null, result) + }, + signTransaction: idStore.addUnconfirmedTransaction.bind(idStore), +}) +var ethStore = new EthStore(zeroClient) +idStore.setStore(ethStore) + +function getState(){ + var state = extend(ethStore.getState(), idStore.getState()) + console.log(state) + return state +} + // handle rpc requests function onRpcRequest(remotePort, payload){ // console.log('MetaMaskPlugin - incoming payload:', payload) @@ -84,8 +66,44 @@ function onRpcRequest(remotePort, payload){ }) } -// setup badge text -wallet.on('update', updateBadge) + +// +// popup integration +// + +function handleInternalCommunication(remotePort){ + var duplex = new PortStream(remotePort) + var connection = Dnode({ + getState: function(cb){ cb(null, getState()) }, + // forward directly to idStore + submitPassword: idStore.submitPassword.bind(idStore), + setSelectedAddress: idStore.setSelectedAddress.bind(idStore), + signTransaction: idStore.signTransaction.bind(idStore), + setLocked: idStore.setLocked.bind(idStore), + }) + duplex.pipe(connection).pipe(duplex) + connection.on('remote', function(remote){ + + // push updates to popup + ethStore.on('update', sendUpdate) + idStore.on('update', sendUpdate) + // teardown on disconnect + eos(duplex, function unsubscribe(){ + ethStore.removeListener('update', sendUpdate) + }) + function sendUpdate(){ + var state = getState() + remote.sendUpdate(state) + } + + }) +} + +// +// plugin badge text +// + +idStore.on('update', updateBadge) function updateBadge(state){ var label = '' @@ -97,17 +115,3 @@ function updateBadge(state){ chrome.browserAction.setBadgeBackgroundColor({color: '#506F8B'}) } -// function handleMessage(msg){ -// console.log('got message!', msg.type) -// switch(msg.type){ - -// case 'addUnsignedTx': -// addTransaction(msg.payload) -// return - -// case 'removeUnsignedTx': -// removeTransaction(msg.payload) -// return - -// } -// } diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js new file mode 100644 index 000000000..0dfc4deed --- /dev/null +++ b/app/scripts/lib/idStore.js @@ -0,0 +1,263 @@ +const EventEmitter = require('events').EventEmitter +const inherits = require('util').inherits +const Transaction = require('ethereumjs-tx') +const KeyStore = require('eth-lightwallet').keystore +const async = require('async') +const clone = require('clone') +const extend = require('xtend') +const createId = require('web3-provider-engine/util/random-id') + + +module.exports = IdentityStore + + +inherits(IdentityStore, EventEmitter) +function IdentityStore(ethStore) { + const self = this + EventEmitter.call(self) + + // we just use the ethStore to auto-add accounts + self._ethStore = ethStore + + self._currentState = { + selectedAddress: null, + identities: {}, + unconfTxs: {}, + } + // not part of serilized metamask state - only kept in memory + self._unconfTxCbs = {} +} + +// +// public +// + +IdentityStore.prototype.setStore = function(store){ + const self = this + self._ethStore = store +} + +IdentityStore.prototype.getState = function(){ + const self = this + return clone(extend(self._currentState, { + isUnlocked: self._isUnlocked(), + })) +} + +IdentityStore.prototype.getSelectedAddress = function(){ + const self = this + return self._currentState.selectedAddress +} + +IdentityStore.prototype.setSelectedAddress = function(address){ + const self = this + self._currentState.selectedAddress = address + self._didUpdate() +} + +IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){ + var self = this + + var time = (new Date()).getTime() + var txId = createId() + self._currentState.unconfTxs[txId] = { + id: txId, + txParams: txParams, + time: time, + status: 'unconfirmed', + } + console.log('addUnconfirmedTransaction:', txParams) + + // temp - just sign the tx + // otherwise we need to keep the cb around + // signTransaction(txId, cb) + self._unconfTxCbs[txId] = cb + + // signal update + self._didUpdate() +} + + + +IdentityStore.prototype.setLocked = function(){ + const self = this + delete self._keyStore + delete window.sessionStorage['password'] +} + +IdentityStore.prototype.submitPassword = function(password, cb){ + const self = this + console.log('submitPassword:', password) + self._tryPassword(password, function(err){ + if (err) console.log('bad password:', password, err) + if (err) return cb(err) + console.log('good password:', password) + window.sessionStorage['password'] = password + // load identities before returning... + self._loadIdentities() + cb() + }) +} + +IdentityStore.prototype.signTransaction = function(password, txId, cb){ + const self = this + + var txData = self._currentState.unconfTxs[txId] + var txParams = txData.txParams + + self._signTransaction(txParams, function(err, rawTx, txHash){ + if (err) { + throw err + txData.status = 'error' + txData.error = err + self._didUpdate() + return + } + + txData.hash = txHash + txData.status = 'pending' + + // for now just remove it + delete self._currentState.unconfTxs[txData.id] + + // rpc callback + var txSigCb = self._unconfTxCbs[txId] || noop + txSigCb(null, rawTx) + + // confirm tx callback + cb() + + self._didUpdate() + }) +} + +// +// private +// + +// internal - actually signs the tx +IdentityStore.prototype._signTransaction = function(txParams, cb){ + const self = this + try { + // console.log('signing tx:', txParams) + var tx = new Transaction({ + nonce: txParams.nonce, + to: txParams.to, + value: txParams.value, + data: txParams.input, + gasPrice: txParams.gasPrice, + gasLimit: txParams.gas, + }) + + var password = self._getPassword() + var serializedTx = self._keyStore.signTx(tx.serialize(), password, self._currentState.selectedAddress) + + // // deserialize and dump values to confirm configuration + // var verifyTx = new Transaction(tx.serialize()) + // console.log('signed transaction:', { + // to: '0x'+verifyTx.to.toString('hex'), + // from: '0x'+verifyTx.from.toString('hex'), + // nonce: '0x'+verifyTx.nonce.toString('hex'), + // value: (ethUtil.bufferToInt(verifyTx.value)/1e18)+' ether', + // data: '0x'+verifyTx.data.toString('hex'), + // gasPrice: '0x'+verifyTx.gasPrice.toString('hex'), + // gasLimit: '0x'+verifyTx.gasLimit.toString('hex'), + // }) + cb(null, serializedTx, tx.hash()) + } catch (err) { + cb(err) + } +} + +IdentityStore.prototype._didUpdate = function(){ + const self = this + self.emit('update', self.getState()) +} + +IdentityStore.prototype._isUnlocked = function(){ + const self = this + // var password = window.sessionStorage['password'] + // var result = Boolean(password) + var result = Boolean(self._keyStore) + return result +} + +// load identities from keyStore +IdentityStore.prototype._loadIdentities = function(){ + const self = this + if (!self._isUnlocked()) throw new Error('not unlocked') + // get addresses and normalize address hexString + var addresses = self._keyStore.getAddresses().map(function(address){ return '0x'+address }) + addresses.forEach(function(address){ + // add to ethStore + self._ethStore.addAccount(address) + // add to identities + var identity = { + name: 'Wally', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: address, + } + self._currentState.identities[address] = identity + }) + self._didUpdate() +} + +// +// keyStore managment - unlocking + deserialization +// + +IdentityStore.prototype._tryPassword = function(password, cb){ + const self = this + var keyStore = self._getKeyStore(password) + var address = keyStore.getAddresses()[0] + if (!address) return cb(new Error('KeyStore - No address to check.')) + var hdPathString = keyStore.defaultHdPathString + try { + var encKey = keyStore.generateEncKey(password) + var encPrivKey = keyStore.ksData[hdPathString].encPrivKeys[address] + var privKey = KeyStore._decryptKey(encPrivKey, encKey) + var addrFromPrivKey = KeyStore._computeAddressFromPrivKey(privKey) + } catch (err) { + return cb(err) + } + if (addrFromPrivKey !== address) return cb(new Error('KeyStore - Decrypting private key failed!')) + cb() +} + +IdentityStore.prototype._getKeyStore = function(password){ + const self = this + var keyStore = null + var serializedKeystore = window.localStorage['lightwallet'] + // returning user + if (serializedKeystore) { + keyStore = KeyStore.deserialize(serializedKeystore) + // first time here + } else { + console.log('creating new keystore with password:', password) + var secretSeed = KeyStore.generateRandomSeed() + keyStore = new KeyStore(secretSeed, password) + keyStore.generateNewAddress(password, 3) + self._saveKeystore(keyStore) + } + keyStore.passwordProvider = function getPassword(cb){ + cb(null, self._getPassword()) + } + self._keyStore = keyStore + return keyStore +} + +IdentityStore.prototype._saveKeystore = function(keyStore){ + const self = this + window.localStorage['lightwallet'] = keyStore.serialize() +} + +IdentityStore.prototype._getPassword = function(){ + const self = this + var password = window.sessionStorage['password'] + console.warn('using password from memory:', password) + return password +} + +// util + +function noop(){} \ No newline at end of file diff --git a/app/scripts/lib/idmgmt.js b/app/scripts/lib/idmgmt.js deleted file mode 100644 index 7bc276041..000000000 --- a/app/scripts/lib/idmgmt.js +++ /dev/null @@ -1,318 +0,0 @@ -const inherits = require('util').inherits -const EventEmitter = require('events').EventEmitter -const async = require('async') -const KeyStore = require('eth-lightwallet').keystore -const createPayload = require('web3-provider-engine/util/create-payload') -const createId = require('web3-provider-engine/util/random-id') -const Transaction = require('ethereumjs-tx') - -module.exports = IdentityManager - - -var selectedAddress = null -var identities = {} -var unconfTxs = {} - -// not part of serilized metamask state - only keep in memory -var unconfTxCbs = {} - -var provider = null -var defaultPassword = 'test' - - - -inherits(IdentityManager, EventEmitter) -function IdentityManager(opts){ - const self = this - self.on('block', function(){ - self.updateIdentities() - }) -} - -// plugin popup -IdentityManager.prototype.getState = getState -IdentityManager.prototype.submitPassword = submitPassword -IdentityManager.prototype.setSelectedAddress = setSelectedAddress -IdentityManager.prototype.signTransaction = signTransaction -IdentityManager.prototype.setLocked = setLocked -// eth rpc -IdentityManager.prototype.getAccounts = getAccounts -IdentityManager.prototype.addUnconfirmedTransaction = addUnconfirmedTransaction -// etc -IdentityManager.prototype.newBlock = newBlock -IdentityManager.prototype.setProvider = setProvider - - - -function setProvider(_provider){ - provider = _provider -} - -function newBlock(block){ - var self = this - self.emit('block', block) -} - -function getState(cb){ - var result = _getState() - cb(null, result) -} - -function _getState(cb){ - var unlocked = isUnlocked() - var result = { - isUnlocked: unlocked, - identities: unlocked ? getIdentities() : {}, - unconfTxs: unlocked ? unconfTxs : {}, - selectedAddress: selectedAddress, - } - return result -} - -function isUnlocked(){ - var password = window.sessionStorage['password'] - var result = Boolean(password) - return result -} - -function setLocked(){ - delete window.sessionStorage['password'] -} - -function setSelectedAddress(address, cb){ - selectedAddress = address - cb(null, _getState()) -} - -function submitPassword(password, cb){ - const self = this - console.log('submitPassword:', password) - tryPassword(password, function(err){ - if (err) console.log('bad password:', password, err) - if (err) return cb(err) - console.log('good password:', password) - window.sessionStorage['password'] = password - // load identities before returning... - self.loadIdentities() - var state = _getState() - cb(null, state) - // trigger an update but dont wait for it - self.updateIdentities() - }) -} - -// get the current selected address -function getAccounts(cb){ - var result = selectedAddress ? [selectedAddress] : [] - cb(null, result) -} - -function getIdentities(){ - return identities -} - -// load identities from keyStore -IdentityManager.prototype.loadIdentities = function(){ - const self = this - if (!isUnlocked()) throw new Error('not unlocked') - var keyStore = getKeyStore() - var addresses = keyStore.getAddresses().map(function(address){ return '0x'+address }) - addresses.forEach(function(address){ - var identity = { - name: 'Wally', - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', - address: address, - balance: null, - txCount: null, - } - identities[address] = identity - }) - self._didUpdate() -} - -IdentityManager.prototype._didUpdate = function(){ - const self = this - self.emit('update', _getState()) -} - -// foreach in identities, update balance + nonce -IdentityManager.prototype.updateIdentities = function(cb){ - var self = this - cb = cb || function(){} - if (!isUnlocked()) return cb(new Error('Not unlocked.')) - var addresses = Object.keys(identities) - async.map(addresses, self.updateIdentity.bind(self), cb) -} - -// gets latest info from the network for the identity -IdentityManager.prototype.updateIdentity = function(address, cb){ - var self = this - async.parallel([ - getAccountBalance.bind(null, address), - getTxCount.bind(null, address), - ], function(err, result){ - if (err) return cb(err) - var identity = identities[address] - identity.balance = result[0] - identity.txCount = result[1] - self._didUpdate() - cb() - }) -} - -function getTxCount(address, cb){ - provider.sendAsync(createPayload({ - method: 'eth_getTransactionCount', - params: [address, 'pending'], - }), function(err, res){ - if (err) return cb(err) - if (res.error) return cb(res.error) - cb(null, res.result) - }) -} - -function getAccountBalance(address, cb){ - provider.sendAsync(createPayload({ - method: 'eth_getBalance', - params: [address, 'latest'], - }), function(err, res){ - if (err) return cb(err) - if (res.error) return cb(res.error) - cb(null, res.result) - }) -} - -function tryPassword(password, cb){ - var keyStore = getKeyStore(password) - var address = keyStore.getAddresses()[0] - if (!address) return cb(new Error('KeyStore - No address to check.')) - var hdPathString = keyStore.defaultHdPathString - try { - var encKey = keyStore.generateEncKey(password) - var encPrivKey = keyStore.ksData[hdPathString].encPrivKeys[address] - var privKey = KeyStore._decryptKey(encPrivKey, encKey) - var addrFromPrivKey = KeyStore._computeAddressFromPrivKey(privKey) - } catch (err) { - return cb(err) - } - if (addrFromPrivKey !== address) return cb(new Error('KeyStore - Decrypting private key failed!')) - cb() -} - -function addUnconfirmedTransaction(txParams, cb){ - var self = this - - var time = (new Date()).getTime() - var txId = createId() - unconfTxs[txId] = { - id: txId, - txParams: txParams, - time: time, - status: 'unconfirmed', - } - console.log('addUnconfirmedTransaction:', txParams) - - // temp - just sign the tx - // otherwise we need to keep the cb around - // signTransaction(txId, cb) - unconfTxCbs[txId] = cb - - // signal update - self._didUpdate() -} - -// called from -function signTransaction(password, txId, cb){ - const self = this - - var txData = unconfTxs[txId] - var txParams = txData.txParams - console.log('signTransaction:', txData) - - self._signTransaction(txParams, function(err, rawTx, txHash){ - if (err) { - txData.status = 'error' - txData.error = err - self._didUpdate() - return - } - txData.hash = txHash - txData.status = 'pending' - // for now just kill it - delete unconfTxs[txData.id] - - var txSigCb = unconfTxCbs[txId] || function(){} - txSigCb(null, rawTx) - - cb(null, _getState()) - self._didUpdate() - }) -} - -// internal - actually signs the tx -IdentityManager.prototype._signTransaction = function(txParams, cb){ - try { - // console.log('signing tx:', txParams) - var tx = new Transaction({ - nonce: txParams.nonce, - to: txParams.to, - value: txParams.value, - data: txParams.input, - gasPrice: txParams.gasPrice, - gasLimit: txParams.gas, - }) - - var keyStore = getKeyStore() - var serializedTx = keyStore.signTx(tx.serialize(), defaultPassword, selectedAddress) - - // // deserialize and dump values to confirm configuration - // var verifyTx = new Transaction(tx.serialize()) - // console.log('signed transaction:', { - // to: '0x'+verifyTx.to.toString('hex'), - // from: '0x'+verifyTx.from.toString('hex'), - // nonce: '0x'+verifyTx.nonce.toString('hex'), - // value: (ethUtil.bufferToInt(verifyTx.value)/1e18)+' ether', - // data: '0x'+verifyTx.data.toString('hex'), - // gasPrice: '0x'+verifyTx.gasPrice.toString('hex'), - // gasLimit: '0x'+verifyTx.gasLimit.toString('hex'), - // }) - cb(null, serializedTx, tx.hash()) - } catch (err) { - cb(err) - } -} - -var keyStore = null -function getKeyStore(password){ - if (keyStore) return keyStore - password = password || getPassword() - var serializedKeystore = window.localStorage['lightwallet'] - // returning user - if (serializedKeystore) { - keyStore = KeyStore.deserialize(serializedKeystore) - // first time here - } else { - console.log('creating new keystore with default password:', defaultPassword) - var secretSeed = KeyStore.generateRandomSeed() - keyStore = new KeyStore(secretSeed, defaultPassword) - keyStore.generateNewAddress(defaultPassword, 3) - saveKeystore() - } - keyStore.passwordProvider = unlockKeystore - return keyStore -} - -function saveKeystore(){ - window.localStorage['lightwallet'] = keyStore.serialize() -} - -function getPassword(){ - var password = window.sessionStorage['password'] - if (!password) throw new Error('No password found...') -} - -function unlockKeystore(cb){ - var password = getPassword() - console.warn('unlocking keystore...') - cb(null, password) -} \ No newline at end of file diff --git a/package.json b/package.json index fd8b5f4a9..4fdd1bc4c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "async": "^1.5.2", + "clone": "^1.0.2", "dnode": "^1.2.2", "end-of-stream": "^1.1.0", "eth-lightwallet": "^1.0.1",