nifty-wallet/app/scripts/lib/idStore.js

485 lines
13 KiB
JavaScript
Raw Normal View History

2016-02-10 17:44:46 -08:00
const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits
const Transaction = require('ethereumjs-tx')
2016-06-16 19:51:34 -07:00
const ethUtil = require('ethereumjs-util')
2016-03-02 22:58:23 -08:00
const LightwalletKeyStore = require('eth-lightwallet').keystore
const LightwalletSigner = require('eth-lightwallet').signing
2016-02-10 17:44:46 -08:00
const async = require('async')
const clone = require('clone')
const extend = require('xtend')
const createId = require('web3-provider-engine/util/random-id')
2016-06-16 19:51:34 -07:00
const ethBinToOps = require('eth-bin-to-ops')
const autoFaucet = require('./auto-faucet')
const configManager = require('./config-manager-singleton')
const messageManager = require('./message-manager')
const DEFAULT_RPC = 'https://testrpc.metamask.io/'
const IdManagement = require('./id-management')
2016-02-10 17:44:46 -08:00
module.exports = IdentityStore
inherits(IdentityStore, EventEmitter)
function IdentityStore(opts = {}) {
EventEmitter.call(this)
2016-02-10 17:44:46 -08:00
// we just use the ethStore to auto-add accounts
this._ethStore = opts.ethStore
2016-03-02 22:58:23 -08:00
// lightwallet key store
this._keyStore = null
2016-03-02 22:58:23 -08:00
// lightwallet wrapper
this._idmgmt = null
this.hdPathString = "m/44'/60'/0'/0"
2016-02-10 17:44:46 -08:00
this._currentState = {
2016-02-10 17:44:46 -08:00
selectedAddress: null,
identities: {},
}
2016-02-10 17:44:46 -08:00
// not part of serilized metamask state - only kept in memory
this._unconfTxCbs = {}
this._unconfMsgCbs = {}
2016-02-10 17:44:46 -08:00
}
//
// public
//
IdentityStore.prototype.createNewVault = function(password, entropy, cb){
delete this._keyStore
configManager.clearWallet()
this._createIdmgmt(password, null, entropy, (err) => {
2016-03-02 22:58:23 -08:00
if (err) return cb(err)
this._loadIdentities()
this._didUpdate()
this._autoFaucet()
configManager.setShowSeedWords(true)
var seedWords = this._idmgmt.getSeed()
2016-03-02 22:58:23 -08:00
cb(null, seedWords)
})
2016-02-17 00:55:57 -08:00
}
IdentityStore.prototype.recoverSeed = function(cb){
configManager.setShowSeedWords(true)
2016-06-02 17:11:10 -07:00
if (!this._idmgmt) return cb(new Error('Unauthenticated. Please sign in.'))
var seedWords = this._idmgmt.getSeed()
cb(null, seedWords)
}
2016-03-15 13:39:12 -07:00
IdentityStore.prototype.recoverFromSeed = function(password, seed, cb){
this._createIdmgmt(password, seed, null, (err) => {
2016-03-15 13:39:12 -07:00
if (err) return cb(err)
this._loadIdentities()
this._didUpdate()
cb(null, this.getState())
2016-03-15 13:39:12 -07:00
})
}
2016-02-17 00:55:57 -08:00
2016-02-10 17:44:46 -08:00
IdentityStore.prototype.setStore = function(store){
this._ethStore = store
2016-02-10 17:44:46 -08:00
}
IdentityStore.prototype.clearSeedWordCache = function(cb) {
configManager.setShowSeedWords(false)
cb(null, configManager.getSelectedAccount())
}
2016-02-17 00:55:57 -08:00
2016-02-10 17:44:46 -08:00
IdentityStore.prototype.getState = function(){
var seedWords = this.getSeedIfUnlocked()
var wallet = configManager.getWallet()
return clone(extend(this._currentState, {
isInitialized: !!configManager.getWallet() && !seedWords,
isUnlocked: this._isUnlocked(),
seedWords: seedWords,
isConfirmed: configManager.getConfirmed(),
unconfTxs: configManager.unconfirmedTxs(),
2016-04-18 17:19:58 -07:00
transactions: configManager.getTxList(),
unconfMsgs: messageManager.unconfirmedMsgs(),
messages: messageManager.getMsgList(),
selectedAddress: configManager.getSelectedAccount(),
2016-02-10 17:44:46 -08:00
}))
}
IdentityStore.prototype.getSeedIfUnlocked = function() {
var showSeed = configManager.getShouldShowSeedWords()
var idmgmt = this._idmgmt
var shouldShow = showSeed && !!idmgmt
var seedWords = shouldShow ? idmgmt.getSeed() : null
return seedWords
}
2016-02-10 17:44:46 -08:00
IdentityStore.prototype.getSelectedAddress = function(){
return configManager.getSelectedAccount()
2016-02-10 17:44:46 -08:00
}
IdentityStore.prototype.setSelectedAddress = function(address, cb){
if (!address) {
var addresses = this._getAddresses()
address = addresses[0]
}
configManager.setSelectedAccount(address)
if (cb) return cb(null, address)
2016-02-10 17:44:46 -08:00
}
IdentityStore.prototype.revealAccount = function(cb) {
let addresses = this._getAddresses()
const derivedKey = this._idmgmt.derivedKey
const keyStore = this._keyStore
keyStore.setDefaultHdDerivationPath(this.hdPathString)
keyStore.generateNewAddress(derivedKey, 1)
configManager.setWallet(keyStore.serialize())
addresses = this._getAddresses()
this._loadIdentities()
this._didUpdate()
cb(null)
}
IdentityStore.prototype.getNetwork = function(err) {
2016-06-03 13:58:09 -07:00
if (err) {
this._currentState.network = 'loading'
2016-06-03 13:58:09 -07:00
this._didUpdate()
2016-05-30 11:22:19 -07:00
}
this.web3.version.getNetwork((err, network) => {
if (err) {
this._currentState.network = 'loading'
return this._didUpdate()
}
console.log('web3.getNetwork returned ' + network)
this._currentState.network = network
2016-06-03 13:58:09 -07:00
this._didUpdate()
})
}
IdentityStore.prototype.setLocked = function(cb){
delete this._keyStore
delete this._idmgmt
cb()
2016-02-10 17:44:46 -08:00
}
IdentityStore.prototype.submitPassword = function(password, cb){
this.tryPassword(password, (err) => {
2016-02-10 17:44:46 -08:00
if (err) return cb(err)
// load identities before returning...
this._loadIdentities()
cb(null, configManager.getSelectedAccount())
2016-02-10 17:44:46 -08:00
})
}
2016-04-06 12:01:10 -07:00
IdentityStore.prototype.exportAccount = function(address, cb) {
var privateKey = this._idmgmt.exportPrivateKey(address)
cb(null, privateKey)
}
//
// Transactions
//
2016-03-02 22:58:23 -08:00
// comes from dapp via zero-client hooked-wallet provider
2016-06-16 19:51:34 -07:00
IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, onTxDoneCb, cb){
var self = this
2016-03-02 22:58:23 -08:00
// create txData obj with parameters and meta data
var time = (new Date()).getTime()
var txId = createId()
txParams.metamaskId = txId
2016-06-16 19:51:34 -07:00
txParams.metamaskNetworkId = self._currentState.network
2016-03-02 22:58:23 -08:00
var txData = {
id: txId,
txParams: txParams,
time: time,
status: 'unconfirmed',
}
configManager.addTx(txData)
console.log('addUnconfirmedTransaction:', txData)
2016-02-10 17:44:46 -08:00
2016-06-16 19:51:34 -07:00
// keep the onTxDoneCb around for after approval/denial (requires user interaction)
// This onTxDoneCb fires completion to the Dapp's write operation.
self._unconfTxCbs[txId] = onTxDoneCb
// perform static analyis on the target contract code
var provider = self._ethStore._query.currentProvider
if (txParams.to) {
provider.sendAsync({ id: 1, method: 'eth_getCode', params: [txParams.to, 'latest'] }, function(err, res){
if (err) return didComplete(err)
if (res.error) return didComplete(res.error)
var code = ethUtil.toBuffer(res.result)
if (code !== '0x') {
var ops = ethBinToOps(code)
var containsDelegateCall = ops.some((op)=>op.name === 'DELEGATECALL')
txData.containsDelegateCall = containsDelegateCall
didComplete()
} else {
didComplete()
}
})
} else {
didComplete()
}
2016-02-10 17:44:46 -08:00
2016-06-16 19:51:34 -07:00
function didComplete(err){
if (err) return cb(err)
// signal update
self._didUpdate()
// signal completion of add tx
cb(null, txData)
}
2016-02-10 17:44:46 -08:00
}
2016-03-02 22:58:23 -08:00
// comes from metamask ui
IdentityStore.prototype.approveTransaction = function(txId, cb){
var txData = configManager.getTx(txId)
var approvalCb = this._unconfTxCbs[txId] || noop
2016-03-02 22:58:23 -08:00
// accept tx
2016-02-12 17:57:10 -08:00
cb()
2016-03-02 22:58:23 -08:00
approvalCb(null, true)
// clean up
configManager.confirmTx(txId)
delete this._unconfTxCbs[txId]
this._didUpdate()
2016-02-12 17:57:10 -08:00
}
2016-03-02 22:58:23 -08:00
// comes from metamask ui
2016-02-12 12:55:20 -08:00
IdentityStore.prototype.cancelTransaction = function(txId){
var txData = configManager.getTx(txId)
var approvalCb = this._unconfTxCbs[txId] || noop
2016-03-02 22:58:23 -08:00
// reject tx
approvalCb(null, false)
// clean up
configManager.rejectTx(txId)
delete this._unconfTxCbs[txId]
this._didUpdate()
2016-02-12 12:55:20 -08:00
}
2016-03-02 22:58:23 -08:00
// performs the actual signing, no autofill of params
IdentityStore.prototype.signTransaction = function(txParams, cb){
2016-02-10 17:44:46 -08:00
try {
2016-03-02 22:58:23 -08:00
console.log('signing tx...', txParams)
var rawTx = this._idmgmt.signTx(txParams)
2016-03-02 22:58:23 -08:00
cb(null, rawTx)
2016-02-10 17:44:46 -08:00
} catch (err) {
cb(err)
}
}
//
// Messages
//
// comes from dapp via zero-client hooked-wallet provider
IdentityStore.prototype.addUnconfirmedMessage = function(msgParams, cb){
// create txData obj with parameters and meta data
var time = (new Date()).getTime()
var msgId = createId()
var msgData = {
id: msgId,
msgParams: msgParams,
time: time,
status: 'unconfirmed',
}
messageManager.addMsg(msgData)
console.log('addUnconfirmedMessage:', msgData)
// keep the cb around for after approval (requires user interaction)
// This cb fires completion to the Dapp's write operation.
this._unconfMsgCbs[msgId] = cb
// signal update
this._didUpdate()
return msgId
}
// comes from metamask ui
IdentityStore.prototype.approveMessage = function(msgId, cb){
var msgData = messageManager.getMsg(msgId)
var approvalCb = this._unconfMsgCbs[msgId] || noop
// accept msg
cb()
approvalCb(null, true)
// clean up
messageManager.confirmMsg(msgId)
delete this._unconfMsgCbs[msgId]
this._didUpdate()
}
// comes from metamask ui
IdentityStore.prototype.cancelMessage = function(msgId){
var txData = messageManager.getMsg(msgId)
var approvalCb = this._unconfMsgCbs[msgId] || noop
// reject tx
approvalCb(null, false)
// clean up
messageManager.rejectMsg(msgId)
delete this._unconfTxCbs[msgId]
this._didUpdate()
}
// performs the actual signing, no autofill of params
IdentityStore.prototype.signMessage = function(msgParams, cb){
try {
console.log('signing msg...', msgParams.data)
var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data)
if ('metamaskId' in msgParams) {
var id = msgParams.metamaskId
delete msgParams.metamaskId
this.approveMessage(id, cb)
} else {
cb(null, rawMsg)
}
} catch (err) {
cb(err)
}
}
2016-03-02 22:58:23 -08:00
//
// private
//
2016-02-10 17:44:46 -08:00
IdentityStore.prototype._didUpdate = function(){
this.emit('update', this.getState())
2016-02-10 17:44:46 -08:00
}
IdentityStore.prototype._isUnlocked = function(){
var result = Boolean(this._keyStore) && Boolean(this._idmgmt)
2016-02-10 17:44:46 -08:00
return result
}
2016-02-17 00:55:57 -08:00
// load identities from keyStoreet
2016-02-10 17:44:46 -08:00
IdentityStore.prototype._loadIdentities = function(){
if (!this._isUnlocked()) throw new Error('not unlocked')
var addresses = this._getAddresses()
addresses.forEach((address, i) => {
2016-02-17 00:55:57 -08:00
// // add to ethStore
this._ethStore.addAccount(address)
2016-02-10 17:44:46 -08:00
// add to identities
const defaultLabel = 'Wallet ' + (i+1)
const nickname = configManager.nicknameForWallet(address)
2016-02-10 17:44:46 -08:00
var identity = {
name: nickname || defaultLabel,
2016-02-10 17:44:46 -08:00
address: address,
mayBeFauceting: this._mayBeFauceting(i),
2016-02-10 17:44:46 -08:00
}
this._currentState.identities[address] = identity
2016-02-10 17:44:46 -08:00
})
this._didUpdate()
2016-02-10 17:44:46 -08:00
}
IdentityStore.prototype.saveAccountLabel = function(account, label, cb) {
configManager.setNicknameForWallet(account, label)
this._loadIdentities()
cb(null, label)
this._didUpdate()
}
// mayBeFauceting
// If on testnet, index 0 may be fauceting.
// The UI will have to check the balance to know.
// If there is no balance and it mayBeFauceting,
// then it is in fact fauceting.
IdentityStore.prototype._mayBeFauceting = function(i) {
var config = configManager.getProvider()
if (i === 0 &&
config.type === 'rpc' &&
config.rpcTarget === DEFAULT_RPC) {
return true
}
return false
}
2016-02-10 17:44:46 -08:00
//
// keyStore managment - unlocking + deserialization
//
IdentityStore.prototype.tryPassword = function(password, cb){
this._createIdmgmt(password, null, null, cb)
2016-02-10 17:44:46 -08:00
}
IdentityStore.prototype._createIdmgmt = function(password, seed, entropy, cb){
2016-02-10 17:44:46 -08:00
var keyStore = null
LightwalletKeyStore.deriveKeyFromPassword(password, (err, derivedKey) => {
2016-03-02 22:58:23 -08:00
if (err) return cb(err)
var serializedKeystore = configManager.getWallet()
2016-03-15 13:39:12 -07:00
if (seed) {
2016-04-01 14:02:02 -07:00
try {
keyStore = this._restoreFromSeed(password, seed, derivedKey)
} catch (e) {
return cb(e)
}
// returning user, recovering from storage
2016-03-15 13:39:12 -07:00
} else if (serializedKeystore) {
2016-03-31 10:24:39 -07:00
keyStore = LightwalletKeyStore.deserialize(serializedKeystore)
var isCorrect = keyStore.isDerivedKeyCorrect(derivedKey)
2016-03-02 22:58:23 -08:00
if (!isCorrect) return cb(new Error('Lightwallet - password incorrect'))
// first time here
2016-03-02 22:58:23 -08:00
} else {
keyStore = this._createFirstWallet(entropy, derivedKey)
2016-03-02 22:58:23 -08:00
}
this._keyStore = keyStore
this._idmgmt = new IdManagement({
keyStore: keyStore,
derivedKey: derivedKey,
hdPathSTring: this.hdPathString,
})
2016-03-02 22:58:23 -08:00
cb()
})
2016-02-10 17:44:46 -08:00
}
IdentityStore.prototype._restoreFromSeed = function(password, seed, derivedKey) {
var keyStore = new LightwalletKeyStore(seed, derivedKey, this.hdPathString)
keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'});
keyStore.setDefaultHdDerivationPath(this.hdPathString)
keyStore.generateNewAddress(derivedKey, 3)
configManager.setWallet(keyStore.serialize())
console.log('restored from seed. saved to keystore')
return keyStore
}
IdentityStore.prototype._createFirstWallet = function(entropy, derivedKey) {
var secretSeed = LightwalletKeyStore.generateRandomSeed(entropy)
var keyStore = new LightwalletKeyStore(secretSeed, derivedKey, this.hdPathString)
keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'});
keyStore.setDefaultHdDerivationPath(this.hdPathString)
keyStore.generateNewAddress(derivedKey, 3)
configManager.setWallet(keyStore.serialize())
console.log('saved to keystore')
return keyStore
}
// get addresses and normalize address hexString
IdentityStore.prototype._getAddresses = function() {
return this._keyStore.getAddresses(this.hdPathString).map((address) => { return '0x'+address })
}
IdentityStore.prototype._autoFaucet = function() {
var addresses = this._getAddresses()
autoFaucet(addresses[0])
}
2016-02-10 17:44:46 -08:00
// util
function noop(){}