1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
metamask-extension/app/scripts/lib/idStore.js
Dan Finlay 7d5aaaa5bd Add ability to add account to vault
Scrolling to the bottom of the accounts page now reveals a downward-facing chevron button.

Pressing this button shows loading indication, adds a new account to the identity vault, displays it in the list, and scrolls the list to the bottom of the page.

Any number of accounts can be generated in this way, and the UX feels intuitive without having to overly explain how HD paths work.
2016-05-20 12:40:44 -07:00

502 lines
14 KiB
JavaScript

const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits
const Transaction = require('ethereumjs-tx')
const LightwalletKeyStore = require('eth-lightwallet').keystore
const LightwalletSigner = require('eth-lightwallet').signing
const async = require('async')
const clone = require('clone')
const extend = require('xtend')
const createId = require('web3-provider-engine/util/random-id')
const autoFaucet = require('./auto-faucet')
const configManager = require('./config-manager-singleton')
const messageManager = require('./message-manager')
const DEFAULT_RPC = 'https://testrpc.metamask.io/'
module.exports = IdentityStore
inherits(IdentityStore, EventEmitter)
function IdentityStore(opts = {}) {
EventEmitter.call(this)
// we just use the ethStore to auto-add accounts
this._ethStore = opts.ethStore
// lightwallet key store
this._keyStore = null
// lightwallet wrapper
this._idmgmt = null
this.hdPathString = "m/44'/60'/0'/0"
this._currentState = {
selectedAddress: null,
identities: {},
}
// not part of serilized metamask state - only kept in memory
this._unconfTxCbs = {}
this._unconfMsgCbs = {}
}
//
// public
//
IdentityStore.prototype.createNewVault = function(password, entropy, cb){
delete this._keyStore
configManager.clearWallet()
this._createIdmgmt(password, null, entropy, (err) => {
if (err) return cb(err)
this._loadIdentities()
this._didUpdate()
this._autoFaucet()
configManager.setShowSeedWords(true)
var seedWords = this._idmgmt.getSeed()
cb(null, seedWords)
})
}
IdentityStore.prototype.recoverFromSeed = function(password, seed, cb){
this._createIdmgmt(password, seed, null, (err) => {
if (err) return cb(err)
this._loadIdentities()
this._didUpdate()
cb(null, this.getState())
})
}
IdentityStore.prototype.setStore = function(store){
this._ethStore = store
}
IdentityStore.prototype.clearSeedWordCache = function(cb) {
configManager.setShowSeedWords(false)
cb(null, configManager.getSelectedAccount())
}
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,
unconfTxs: configManager.unconfirmedTxs(),
transactions: configManager.getTxList(),
unconfMsgs: messageManager.unconfirmedMsgs(),
messages: messageManager.getMsgList(),
selectedAddress: configManager.getSelectedAccount(),
}))
}
IdentityStore.prototype.getSeedIfUnlocked = function() {
var showSeed = configManager.getShouldShowSeedWords()
var idmgmt = this._idmgmt
var shouldShow = showSeed && !!idmgmt
var seedWords = shouldShow ? idmgmt.getSeed() : null
return seedWords
}
IdentityStore.prototype.getSelectedAddress = function(){
return configManager.getSelectedAccount()
}
IdentityStore.prototype.setSelectedAddress = function(address, cb){
if (!address) {
var addresses = this._getAddresses()
address = addresses[0]
}
configManager.setSelectedAccount(address)
if (cb) return cb(null, address)
}
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(tries) {
if (tries === 0) return
this.web3.version.getNetwork((err, network) => {
if (err) {
return this.getNetwork(tries - 1, cb)
}
this._currentState.network = network
})
}
IdentityStore.prototype.setLocked = function(cb){
delete this._keyStore
delete this._idmgmt
cb()
}
IdentityStore.prototype.submitPassword = function(password, cb){
this._tryPassword(password, (err) => {
if (err) return cb(err)
// load identities before returning...
this._loadIdentities()
cb(null, configManager.getSelectedAccount())
})
}
IdentityStore.prototype.exportAccount = function(address, cb) {
var privateKey = this._idmgmt.exportPrivateKey(address)
cb(null, privateKey)
}
//
// Transactions
//
// comes from dapp via zero-client hooked-wallet provider
IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){
// create txData obj with parameters and meta data
var time = (new Date()).getTime()
var txId = createId()
txParams.metamaskId = txId
txParams.metamaskNetworkId = this._currentState.network
var txData = {
id: txId,
txParams: txParams,
time: time,
status: 'unconfirmed',
}
configManager.addTx(txData)
console.log('addUnconfirmedTransaction:', txData)
// keep the cb around for after approval (requires user interaction)
// This cb fires completion to the Dapp's write operation.
this._unconfTxCbs[txId] = cb
// signal update
this._didUpdate()
return txId
}
// comes from metamask ui
IdentityStore.prototype.approveTransaction = function(txId, cb){
var txData = configManager.getTx(txId)
var approvalCb = this._unconfTxCbs[txId] || noop
// accept tx
cb()
approvalCb(null, true)
// clean up
configManager.confirmTx(txId)
delete this._unconfTxCbs[txId]
this._didUpdate()
}
// comes from metamask ui
IdentityStore.prototype.cancelTransaction = function(txId){
var txData = configManager.getTx(txId)
var approvalCb = this._unconfTxCbs[txId] || noop
// reject tx
approvalCb(null, false)
// clean up
configManager.rejectTx(txId)
delete this._unconfTxCbs[txId]
this._didUpdate()
}
// performs the actual signing, no autofill of params
IdentityStore.prototype.signTransaction = function(txParams, cb){
try {
console.log('signing tx...', txParams)
var rawTx = this._idmgmt.signTx(txParams)
cb(null, rawTx)
} 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)
}
}
//
// private
//
IdentityStore.prototype._didUpdate = function(){
this.emit('update', this.getState())
}
IdentityStore.prototype._isUnlocked = function(){
var result = Boolean(this._keyStore) && Boolean(this._idmgmt)
return result
}
// load identities from keyStoreet
IdentityStore.prototype._loadIdentities = function(){
if (!this._isUnlocked()) throw new Error('not unlocked')
var addresses = this._getAddresses()
addresses.forEach((address, i) => {
// // add to ethStore
this._ethStore.addAccount(address)
// add to identities
var identity = {
name: 'Wallet ' + (i+1),
img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd',
address: address,
mayBeFauceting: this._mayBeFauceting(i),
}
this._currentState.identities[address] = identity
})
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
}
//
// keyStore managment - unlocking + deserialization
//
IdentityStore.prototype._tryPassword = function(password, cb){
this._createIdmgmt(password, null, null, cb)
}
IdentityStore.prototype._createIdmgmt = function(password, seed, entropy, cb){
var keyStore = null
LightwalletKeyStore.deriveKeyFromPassword(password, (err, derivedKey) => {
if (err) return cb(err)
var serializedKeystore = configManager.getWallet()
if (seed) {
try {
keyStore = this._restoreFromSeed(password, seed, derivedKey)
} catch (e) {
return cb(e)
}
// returning user, recovering from storage
} else if (serializedKeystore) {
keyStore = LightwalletKeyStore.deserialize(serializedKeystore)
var isCorrect = keyStore.isDerivedKeyCorrect(derivedKey)
if (!isCorrect) return cb(new Error('Lightwallet - password incorrect'))
// first time here
} else {
keyStore = this._createFirstWallet(entropy, derivedKey)
}
this._keyStore = keyStore
this._idmgmt = new IdManagement({
keyStore: keyStore,
derivedKey: derivedKey,
hdPathSTring: this.hdPathString,
})
cb()
})
}
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])
}
function IdManagement(opts) {
if (!opts) opts = {}
this.keyStore = opts.keyStore
this.derivedKey = opts.derivedKey
this.hdPathString = "m/44'/60'/0'/0"
this.getAddresses = function(){
return keyStore.getAddresses(this.hdPathString).map(function(address){ return '0x'+address })
}
this.signTx = function(txParams){
// normalize values
txParams.to = ethUtil.addHexPrefix(txParams.to)
txParams.from = ethUtil.addHexPrefix(txParams.from)
txParams.value = ethUtil.addHexPrefix(txParams.value)
txParams.data = ethUtil.addHexPrefix(txParams.data)
txParams.gasLimit = ethUtil.addHexPrefix(txParams.gasLimit || txParams.gas)
txParams.nonce = ethUtil.addHexPrefix(txParams.nonce)
var tx = new Transaction(txParams)
// sign tx
var privKeyHex = this.exportPrivateKey(txParams.from)
var privKey = ethUtil.toBuffer(privKeyHex)
tx.sign(privKey)
// Add the tx hash to the persisted meta-tx object
var txHash = ethUtil.bufferToHex(tx.hash())
var metaTx = configManager.getTx(txParams.metamaskId)
metaTx.hash = txHash
configManager.updateTx(metaTx)
// return raw serialized tx
var rawTx = ethUtil.bufferToHex(tx.serialize())
return rawTx
}
this.signMsg = function(address, message){
// sign message
var privKeyHex = this.exportPrivateKey(address)
var privKey = ethUtil.toBuffer(privKeyHex)
var msgHash = ethUtil.sha3(message)
var msgSig = ethUtil.ecsign(msgHash, privKey)
var rawMsgSig = ethUtil.bufferToHex(concatSig(msgSig.v, msgSig.r, msgSig.s))
return rawMsgSig
}
this.getSeed = function(){
return this.keyStore.getSeed(this.derivedKey)
}
this.exportPrivateKey = function(address) {
var privKeyHex = ethUtil.addHexPrefix(this.keyStore.exportPrivateKey(address, this.derivedKey, this.hdPathString))
return privKeyHex
}
}
// util
function noop(){}
function concatSig(v, r, s) {
r = ethUtil.fromSigned(r)
s = ethUtil.fromSigned(s)
v = ethUtil.bufferToInt(v)
r = ethUtil.toUnsigned(r).toString('hex')
s = ethUtil.toUnsigned(s).toString('hex')
v = ethUtil.stripHexPrefix(ethUtil.intToHex(v))
return ethUtil.addHexPrefix(r.concat(s, v).toString("hex"))
}