mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
7d5aaaa5bd
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.
502 lines
14 KiB
JavaScript
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"))
|
|
}
|