You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
486 lines
13 KiB
486 lines
13 KiB
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.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"))
|
|
}
|
|
|