commit
ee62a6a391
@ -0,0 +1,680 @@ |
||||
const ethUtil = require('ethereumjs-util') |
||||
const bip39 = require('bip39') |
||||
const EventEmitter = require('events').EventEmitter |
||||
const filter = require('promise-filter') |
||||
const encryptor = require('browser-passworder') |
||||
|
||||
const normalize = require('./lib/sig-util').normalize |
||||
const messageManager = require('./lib/message-manager') |
||||
const BN = ethUtil.BN |
||||
|
||||
// Keyrings:
|
||||
const SimpleKeyring = require('./keyrings/simple') |
||||
const HdKeyring = require('./keyrings/hd') |
||||
const keyringTypes = [ |
||||
SimpleKeyring, |
||||
HdKeyring, |
||||
] |
||||
|
||||
const createId = require('./lib/random-id') |
||||
|
||||
module.exports = class KeyringController extends EventEmitter { |
||||
|
||||
// PUBLIC METHODS
|
||||
//
|
||||
// THE FIRST SECTION OF METHODS ARE PUBLIC-FACING,
|
||||
// MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS.
|
||||
//
|
||||
// THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE.
|
||||
|
||||
constructor (opts) { |
||||
super() |
||||
this.configManager = opts.configManager |
||||
this.ethStore = opts.ethStore |
||||
this.encryptor = encryptor |
||||
this.keyringTypes = keyringTypes |
||||
this.keyrings = [] |
||||
this.identities = {} // Essentially a name hash
|
||||
|
||||
this._unconfMsgCbs = {} |
||||
|
||||
this.getNetwork = opts.getNetwork |
||||
} |
||||
|
||||
// Set Store
|
||||
//
|
||||
// Allows setting the ethStore after the constructor.
|
||||
// This is currently required because of the initialization order
|
||||
// of the ethStore and this class.
|
||||
//
|
||||
// Eventually would be nice to be able to add this in the constructor.
|
||||
setStore (ethStore) { |
||||
this.ethStore = ethStore |
||||
} |
||||
|
||||
// Full Update
|
||||
// returns Promise( @object state )
|
||||
//
|
||||
// Emits the `update` event and
|
||||
// returns a Promise that resolves to the current state.
|
||||
//
|
||||
// Frequently used to end asynchronous chains in this class,
|
||||
// indicating consumers can often either listen for updates,
|
||||
// or accept a state-resolving promise to consume their results.
|
||||
//
|
||||
// Not all methods end with this, that might be a nice refactor.
|
||||
fullUpdate () { |
||||
this.emit('update') |
||||
return Promise.resolve(this.getState()) |
||||
} |
||||
|
||||
// Get State
|
||||
// returns @object state
|
||||
//
|
||||
// This method returns a hash representing the current state
|
||||
// that the keyringController manages.
|
||||
//
|
||||
// It is extended in the MetamaskController along with the EthStore
|
||||
// state, and its own state, to create the metamask state branch
|
||||
// that is passed to the UI.
|
||||
//
|
||||
// This is currently a rare example of a synchronously resolving method
|
||||
// in this class, but will need to be Promisified when we move our
|
||||
// persistence to an async model.
|
||||
getState () { |
||||
const configManager = this.configManager |
||||
const address = configManager.getSelectedAccount() |
||||
const wallet = configManager.getWallet() // old style vault
|
||||
const vault = configManager.getVault() // new style vault
|
||||
const keyrings = this.keyrings |
||||
|
||||
return Promise.all(keyrings.map(this.displayForKeyring)) |
||||
.then((displayKeyrings) => { |
||||
return { |
||||
seedWords: this.configManager.getSeedWords(), |
||||
isInitialized: (!!wallet || !!vault), |
||||
isUnlocked: Boolean(this.password), |
||||
isDisclaimerConfirmed: this.configManager.getConfirmedDisclaimer(), |
||||
unconfMsgs: messageManager.unconfirmedMsgs(), |
||||
messages: messageManager.getMsgList(), |
||||
selectedAccount: address, |
||||
shapeShiftTxList: this.configManager.getShapeShiftTxList(), |
||||
currentFiat: this.configManager.getCurrentFiat(), |
||||
conversionRate: this.configManager.getConversionRate(), |
||||
conversionDate: this.configManager.getConversionDate(), |
||||
keyringTypes: this.keyringTypes.map(krt => krt.type), |
||||
identities: this.identities, |
||||
keyrings: displayKeyrings, |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// Create New Vault And Keychain
|
||||
// @string password - The password to encrypt the vault with
|
||||
//
|
||||
// returns Promise( @object state )
|
||||
//
|
||||
// Destroys any old encrypted storage,
|
||||
// creates a new encrypted store with the given password,
|
||||
// randomly creates a new HD wallet with 1 account,
|
||||
// faucets that account on the testnet.
|
||||
createNewVaultAndKeychain (password) { |
||||
return this.persistAllKeyrings(password) |
||||
.then(this.createFirstKeyTree.bind(this)) |
||||
.then(this.fullUpdate.bind(this)) |
||||
} |
||||
|
||||
// CreateNewVaultAndRestore
|
||||
// @string password - The password to encrypt the vault with
|
||||
// @string seed - The BIP44-compliant seed phrase.
|
||||
//
|
||||
// returns Promise( @object state )
|
||||
//
|
||||
// Destroys any old encrypted storage,
|
||||
// creates a new encrypted store with the given password,
|
||||
// creates a new HD wallet from the given seed with 1 account.
|
||||
createNewVaultAndRestore (password, seed) { |
||||
if (typeof password !== 'string') { |
||||
return Promise.reject('Password must be text.') |
||||
} |
||||
|
||||
if (!bip39.validateMnemonic(seed)) { |
||||
return Promise.reject('Seed phrase is invalid.') |
||||
} |
||||
|
||||
this.clearKeyrings() |
||||
|
||||
return this.persistAllKeyrings(password) |
||||
.then(() => { |
||||
return this.addNewKeyring('HD Key Tree', { |
||||
mnemonic: seed, |
||||
numberOfAccounts: 1, |
||||
}) |
||||
}).then(() => { |
||||
const firstKeyring = this.keyrings[0] |
||||
return firstKeyring.getAccounts() |
||||
}) |
||||
.then((accounts) => { |
||||
const firstAccount = accounts[0] |
||||
const hexAccount = normalize(firstAccount) |
||||
this.configManager.setSelectedAccount(hexAccount) |
||||
return this.setupAccounts(accounts) |
||||
}) |
||||
.then(this.persistAllKeyrings.bind(this, password)) |
||||
.then(this.fullUpdate.bind(this)) |
||||
} |
||||
|
||||
// PlaceSeedWords
|
||||
// returns Promise( @object state )
|
||||
//
|
||||
// Adds the current vault's seed words to the UI's state tree.
|
||||
//
|
||||
// Used when creating a first vault, to allow confirmation.
|
||||
// Also used when revealing the seed words in the confirmation view.
|
||||
placeSeedWords () { |
||||
const firstKeyring = this.keyrings[0] |
||||
return firstKeyring.serialize() |
||||
.then((serialized) => { |
||||
const seedWords = serialized.mnemonic |
||||
this.configManager.setSeedWords(seedWords) |
||||
return this.fullUpdate() |
||||
}) |
||||
} |
||||
|
||||
// ClearSeedWordCache
|
||||
//
|
||||
// returns Promise( @string currentSelectedAccount )
|
||||
//
|
||||
// Removes the current vault's seed words from the UI's state tree,
|
||||
// ensuring they are only ever available in the background process.
|
||||
clearSeedWordCache () { |
||||
this.configManager.setSeedWords(null) |
||||
return Promise.resolve(this.configManager.getSelectedAccount()) |
||||
} |
||||
|
||||
// Set Locked
|
||||
// returns Promise( @object state )
|
||||
//
|
||||
// This method deallocates all secrets, and effectively locks metamask.
|
||||
setLocked () { |
||||
this.password = null |
||||
this.keyrings = [] |
||||
return this.fullUpdate() |
||||
} |
||||
|
||||
// Submit Password
|
||||
// @string password
|
||||
//
|
||||
// returns Promise( @object state )
|
||||
//
|
||||
// Attempts to decrypt the current vault and load its keyrings
|
||||
// into memory.
|
||||
//
|
||||
// Temporarily also migrates any old-style vaults first, as well.
|
||||
// (Pre MetaMask 3.0.0)
|
||||
submitPassword (password) { |
||||
return this.unlockKeyrings(password) |
||||
.then((keyrings) => { |
||||
this.keyrings = keyrings |
||||
return this.fullUpdate() |
||||
}) |
||||
} |
||||
|
||||
// Add New Keyring
|
||||
// @string type
|
||||
// @object opts
|
||||
//
|
||||
// returns Promise( @Keyring keyring )
|
||||
//
|
||||
// Adds a new Keyring of the given `type` to the vault
|
||||
// and the current decrypted Keyrings array.
|
||||
//
|
||||
// All Keyring classes implement a unique `type` string,
|
||||
// and this is used to retrieve them from the keyringTypes array.
|
||||
addNewKeyring (type, opts) { |
||||
const Keyring = this.getKeyringClassForType(type) |
||||
const keyring = new Keyring(opts) |
||||
return keyring.getAccounts() |
||||
.then((accounts) => { |
||||
this.keyrings.push(keyring) |
||||
return this.setupAccounts(accounts) |
||||
}) |
||||
.then(() => { return this.password }) |
||||
.then(this.persistAllKeyrings.bind(this)) |
||||
.then(() => { |
||||
return keyring |
||||
}) |
||||
} |
||||
|
||||
// Add New Account
|
||||
// @number keyRingNum
|
||||
//
|
||||
// returns Promise( @object state )
|
||||
//
|
||||
// Calls the `addAccounts` method on the Keyring
|
||||
// in the kryings array at index `keyringNum`,
|
||||
// and then saves those changes.
|
||||
addNewAccount (keyRingNum = 0) { |
||||
const ring = this.keyrings[keyRingNum] |
||||
return ring.addAccounts(1) |
||||
.then(this.setupAccounts.bind(this)) |
||||
.then(this.persistAllKeyrings.bind(this)) |
||||
.then(this.fullUpdate.bind(this)) |
||||
} |
||||
|
||||
// Set Selected Account
|
||||
// @string address
|
||||
//
|
||||
// returns Promise( @string address )
|
||||
//
|
||||
// Sets the state's `selectedAccount` value
|
||||
// to the specified address.
|
||||
setSelectedAccount (address) { |
||||
var addr = normalize(address) |
||||
this.configManager.setSelectedAccount(addr) |
||||
return this.fullUpdate() |
||||
} |
||||
|
||||
// Save Account Label
|
||||
// @string account
|
||||
// @string label
|
||||
//
|
||||
// returns Promise( @string label )
|
||||
//
|
||||
// Persists a nickname equal to `label` for the specified account.
|
||||
saveAccountLabel (account, label) { |
||||
const address = normalize(account) |
||||
const configManager = this.configManager |
||||
configManager.setNicknameForWallet(address, label) |
||||
this.identities[address].name = label |
||||
return Promise.resolve(label) |
||||
} |
||||
|
||||
// Export Account
|
||||
// @string address
|
||||
//
|
||||
// returns Promise( @string privateKey )
|
||||
//
|
||||
// Requests the private key from the keyring controlling
|
||||
// the specified address.
|
||||
//
|
||||
// Returns a Promise that may resolve with the private key string.
|
||||
exportAccount (address) { |
||||
try { |
||||
return this.getKeyringForAccount(address) |
||||
.then((keyring) => { |
||||
return keyring.exportAccount(normalize(address)) |
||||
}) |
||||
} catch (e) { |
||||
return Promise.reject(e) |
||||
} |
||||
} |
||||
|
||||
|
||||
// SIGNING METHODS
|
||||
//
|
||||
// This method signs tx and returns a promise for
|
||||
// TX Manager to update the state after signing
|
||||
|
||||
signTransaction (ethTx, _fromAddress) { |
||||
const fromAddress = normalize(_fromAddress) |
||||
return this.getKeyringForAccount(fromAddress) |
||||
.then((keyring) => { |
||||
return keyring.signTransaction(fromAddress, ethTx) |
||||
}) |
||||
} |
||||
// Add Unconfirmed Message
|
||||
// @object msgParams
|
||||
// @function cb
|
||||
//
|
||||
// Does not call back, only emits an `update` event.
|
||||
//
|
||||
// Adds the given `msgParams` and `cb` to a local cache,
|
||||
// for displaying to a user for approval before signing or canceling.
|
||||
addUnconfirmedMessage (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.emit('update') |
||||
return msgId |
||||
} |
||||
|
||||
// Cancel Message
|
||||
// @string msgId
|
||||
// @function cb (optional)
|
||||
//
|
||||
// Calls back to cached `unconfMsgCb`.
|
||||
// Calls back to `cb` if provided.
|
||||
//
|
||||
// Forgets any messages matching `msgId`.
|
||||
cancelMessage (msgId, cb) { |
||||
var approvalCb = this._unconfMsgCbs[msgId] || noop |
||||
|
||||
// reject tx
|
||||
approvalCb(null, false) |
||||
// clean up
|
||||
messageManager.rejectMsg(msgId) |
||||
delete this._unconfTxCbs[msgId] |
||||
|
||||
if (cb && typeof cb === 'function') { |
||||
cb() |
||||
} |
||||
} |
||||
|
||||
// Sign Message
|
||||
// @object msgParams
|
||||
// @function cb
|
||||
//
|
||||
// returns Promise(@buffer rawSig)
|
||||
// calls back @function cb with @buffer rawSig
|
||||
// calls back cached Dapp's @function unconfMsgCb.
|
||||
//
|
||||
// Attempts to sign the provided @object msgParams.
|
||||
signMessage (msgParams, cb) { |
||||
try { |
||||
const msgId = msgParams.metamaskId |
||||
delete msgParams.metamaskId |
||||
const approvalCb = this._unconfMsgCbs[msgId] || noop |
||||
|
||||
const address = normalize(msgParams.from) |
||||
return this.getKeyringForAccount(address) |
||||
.then((keyring) => { |
||||
return keyring.signMessage(address, msgParams.data) |
||||
}).then((rawSig) => { |
||||
cb(null, rawSig) |
||||
approvalCb(null, true) |
||||
return rawSig |
||||
}) |
||||
} catch (e) { |
||||
cb(e) |
||||
} |
||||
} |
||||
|
||||
// PRIVATE METHODS
|
||||
//
|
||||
// THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER
|
||||
// AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS.
|
||||
|
||||
// Create First Key Tree
|
||||
// returns @Promise
|
||||
//
|
||||
// Clears the vault,
|
||||
// creates a new one,
|
||||
// creates a random new HD Keyring with 1 account,
|
||||
// makes that account the selected account,
|
||||
// faucets that account on testnet,
|
||||
// puts the current seed words into the state tree.
|
||||
createFirstKeyTree () { |
||||
this.clearKeyrings() |
||||
return this.addNewKeyring('HD Key Tree', {numberOfAccounts: 1}) |
||||
.then(() => { |
||||
return this.keyrings[0].getAccounts() |
||||
}) |
||||
.then((accounts) => { |
||||
const firstAccount = accounts[0] |
||||
const hexAccount = normalize(firstAccount) |
||||
this.configManager.setSelectedAccount(hexAccount) |
||||
this.emit('newAccount', hexAccount) |
||||
return this.setupAccounts(accounts) |
||||
}).then(() => { |
||||
return this.placeSeedWords() |
||||
}) |
||||
.then(this.persistAllKeyrings.bind(this)) |
||||
} |
||||
|
||||
// Setup Accounts
|
||||
// @array accounts
|
||||
//
|
||||
// returns @Promise(@object account)
|
||||
//
|
||||
// Initializes the provided account array
|
||||
// Gives them numerically incremented nicknames,
|
||||
// and adds them to the ethStore for regular balance checking.
|
||||
setupAccounts (accounts) { |
||||
return this.getAccounts() |
||||
.then((loadedAccounts) => { |
||||
const arr = accounts || loadedAccounts |
||||
return Promise.all(arr.map((account) => { |
||||
return this.getBalanceAndNickname(account) |
||||
})) |
||||
}) |
||||
} |
||||
|
||||
// Get Balance And Nickname
|
||||
// @string account
|
||||
//
|
||||
// returns Promise( @string label )
|
||||
//
|
||||
// Takes an account address and an iterator representing
|
||||
// the current number of named accounts.
|
||||
getBalanceAndNickname (account) { |
||||
if (!account) { |
||||
throw new Error('Problem loading account.') |
||||
} |
||||
const address = normalize(account) |
||||
this.ethStore.addAccount(address) |
||||
return this.createNickname(address) |
||||
} |
||||
|
||||
// Create Nickname
|
||||
// @string address
|
||||
//
|
||||
// returns Promise( @string label )
|
||||
//
|
||||
// Takes an address, and assigns it an incremented nickname, persisting it.
|
||||
createNickname (address) { |
||||
const hexAddress = normalize(address) |
||||
var i = Object.keys(this.identities).length |
||||
const oldNickname = this.configManager.nicknameForWallet(address) |
||||
const name = oldNickname || `Account ${++i}` |
||||
this.identities[hexAddress] = { |
||||
address: hexAddress, |
||||
name, |
||||
} |
||||
return this.saveAccountLabel(hexAddress, name) |
||||
} |
||||
|
||||
// Persist All Keyrings
|
||||
// @password string
|
||||
//
|
||||
// returns Promise
|
||||
//
|
||||
// Iterates the current `keyrings` array,
|
||||
// serializes each one into a serialized array,
|
||||
// encrypts that array with the provided `password`,
|
||||
// and persists that encrypted string to storage.
|
||||
persistAllKeyrings (password = this.password) { |
||||
if (typeof password === 'string') { |
||||
this.password = password |
||||
} |
||||
return Promise.all(this.keyrings.map((keyring) => { |
||||
return Promise.all([keyring.type, keyring.serialize()]) |
||||
.then((serializedKeyringArray) => { |
||||
// Label the output values on each serialized Keyring:
|
||||
return { |
||||
type: serializedKeyringArray[0], |
||||
data: serializedKeyringArray[1], |
||||
} |
||||
}) |
||||
})) |
||||
.then((serializedKeyrings) => { |
||||
return this.encryptor.encrypt(this.password, serializedKeyrings) |
||||
}) |
||||
.then((encryptedString) => { |
||||
this.configManager.setVault(encryptedString) |
||||
return true |
||||
}) |
||||
} |
||||
|
||||
// Unlock Keyrings
|
||||
// @string password
|
||||
//
|
||||
// returns Promise( @array keyrings )
|
||||
//
|
||||
// Attempts to unlock the persisted encrypted storage,
|
||||
// initializing the persisted keyrings to RAM.
|
||||
unlockKeyrings (password) { |
||||
const encryptedVault = this.configManager.getVault() |
||||
if (!encryptedVault) { |
||||
throw new Error('Cannot unlock without a previous vault.') |
||||
} |
||||
|
||||
return this.encryptor.decrypt(password, encryptedVault) |
||||
.then((vault) => { |
||||
this.password = password |
||||
vault.forEach(this.restoreKeyring.bind(this)) |
||||
return this.keyrings |
||||
}) |
||||
} |
||||
|
||||
// Restore Keyring
|
||||
// @object serialized
|
||||
//
|
||||
// returns Promise( @Keyring deserialized )
|
||||
//
|
||||
// Attempts to initialize a new keyring from the provided
|
||||
// serialized payload.
|
||||
//
|
||||
// On success, returns the resulting @Keyring instance.
|
||||
restoreKeyring (serialized) { |
||||
const { type, data } = serialized |
||||
|
||||
const Keyring = this.getKeyringClassForType(type) |
||||
const keyring = new Keyring() |
||||
return keyring.deserialize(data) |
||||
.then(() => { |
||||
return keyring.getAccounts() |
||||
}) |
||||
.then((accounts) => { |
||||
return this.setupAccounts(accounts) |
||||
}) |
||||
.then(() => { |
||||
this.keyrings.push(keyring) |
||||
return keyring |
||||
}) |
||||
} |
||||
|
||||
// Get Keyring Class For Type
|
||||
// @string type
|
||||
//
|
||||
// Returns @class Keyring
|
||||
//
|
||||
// Searches the current `keyringTypes` array
|
||||
// for a Keyring class whose unique `type` property
|
||||
// matches the provided `type`,
|
||||
// returning it if it exists.
|
||||
getKeyringClassForType (type) { |
||||
return this.keyringTypes.find(kr => kr.type === type) |
||||
} |
||||
|
||||
// Get Accounts
|
||||
// returns Promise( @Array[ @string accounts ] )
|
||||
//
|
||||
// Returns the public addresses of all current accounts
|
||||
// managed by all currently unlocked keyrings.
|
||||
getAccounts () { |
||||
const keyrings = this.keyrings || [] |
||||
return Promise.all(keyrings.map(kr => kr.getAccounts())) |
||||
.then((keyringArrays) => { |
||||
return keyringArrays.reduce((res, arr) => { |
||||
return res.concat(arr) |
||||
}, []) |
||||
}) |
||||
} |
||||
|
||||
// Get Keyring For Account
|
||||
// @string address
|
||||
//
|
||||
// returns Promise(@Keyring keyring)
|
||||
//
|
||||
// Returns the currently initialized keyring that manages
|
||||
// the specified `address` if one exists.
|
||||
getKeyringForAccount (address) { |
||||
const hexed = normalize(address) |
||||
|
||||
return Promise.all(this.keyrings.map((keyring) => { |
||||
return Promise.all([ |
||||
keyring, |
||||
keyring.getAccounts(), |
||||
]) |
||||
})) |
||||
.then(filter((candidate) => { |
||||
const accounts = candidate[1].map(normalize) |
||||
return accounts.includes(hexed) |
||||
})) |
||||
.then((winners) => { |
||||
if (winners && winners.length > 0) { |
||||
return winners[0][0] |
||||
} else { |
||||
throw new Error('No keyring found for the requested account.') |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// Display For Keyring
|
||||
// @Keyring keyring
|
||||
//
|
||||
// returns Promise( @Object { type:String, accounts:Array } )
|
||||
//
|
||||
// Is used for adding the current keyrings to the state object.
|
||||
displayForKeyring (keyring) { |
||||
return keyring.getAccounts() |
||||
.then((accounts) => { |
||||
return { |
||||
type: keyring.type, |
||||
accounts: accounts, |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// Add Gas Buffer
|
||||
// @string gas (as hexadecimal value)
|
||||
//
|
||||
// returns @string bufferedGas (as hexadecimal value)
|
||||
//
|
||||
// Adds a healthy buffer of gas to an initial gas estimate.
|
||||
addGasBuffer (gas) { |
||||
const gasBuffer = new BN('100000', 10) |
||||
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) |
||||
const correct = bnGas.add(gasBuffer) |
||||
return ethUtil.addHexPrefix(correct.toString(16)) |
||||
} |
||||
|
||||
// Clear Keyrings
|
||||
//
|
||||
// Deallocates all currently managed keyrings and accounts.
|
||||
// Used before initializing a new vault.
|
||||
clearKeyrings () { |
||||
let accounts |
||||
try { |
||||
accounts = Object.keys(this.ethStore._currentState.accounts) |
||||
} catch (e) { |
||||
accounts = [] |
||||
} |
||||
accounts.forEach((address) => { |
||||
this.ethStore.removeAccount(address) |
||||
}) |
||||
|
||||
this.keyrings = [] |
||||
this.identities = {} |
||||
this.configManager.setSelectedAccount() |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
function noop () {} |
@ -0,0 +1,111 @@ |
||||
const EventEmitter = require('events').EventEmitter |
||||
const hdkey = require('ethereumjs-wallet/hdkey') |
||||
const bip39 = require('bip39') |
||||
const ethUtil = require('ethereumjs-util') |
||||
|
||||
// *Internal Deps
|
||||
const sigUtil = require('../lib/sig-util') |
||||
|
||||
// Options:
|
||||
const hdPathString = `m/44'/60'/0'/0` |
||||
const type = 'HD Key Tree' |
||||
|
||||
class HdKeyring extends EventEmitter { |
||||
|
||||
/* PUBLIC METHODS */ |
||||
|
||||
constructor (opts = {}) { |
||||
super() |
||||
this.type = type |
||||
this.deserialize(opts) |
||||
} |
||||
|
||||
serialize () { |
||||
return Promise.resolve({ |
||||
mnemonic: this.mnemonic, |
||||
numberOfAccounts: this.wallets.length, |
||||
}) |
||||
} |
||||
|
||||
deserialize (opts = {}) { |
||||
this.opts = opts || {} |
||||
this.wallets = [] |
||||
this.mnemonic = null |
||||
this.root = null |
||||
|
||||
if (opts.mnemonic) { |
||||
this._initFromMnemonic(opts.mnemonic) |
||||
} |
||||
|
||||
if (opts.numberOfAccounts) { |
||||
return this.addAccounts(opts.numberOfAccounts) |
||||
} |
||||
|
||||
return Promise.resolve([]) |
||||
} |
||||
|
||||
addAccounts (numberOfAccounts = 1) { |
||||
if (!this.root) { |
||||
this._initFromMnemonic(bip39.generateMnemonic()) |
||||
} |
||||
|
||||
const oldLen = this.wallets.length |
||||
const newWallets = [] |
||||
for (let i = oldLen; i < numberOfAccounts + oldLen; i++) { |
||||
const child = this.root.deriveChild(i) |
||||
const wallet = child.getWallet() |
||||
newWallets.push(wallet) |
||||
this.wallets.push(wallet) |
||||
} |
||||
const hexWallets = newWallets.map(w => w.getAddress().toString('hex')) |
||||
return Promise.resolve(hexWallets) |
||||
} |
||||
|
||||
getAccounts () { |
||||
return Promise.resolve(this.wallets.map(w => w.getAddress().toString('hex'))) |
||||
} |
||||
|
||||
// tx is an instance of the ethereumjs-transaction class.
|
||||
signTransaction (address, tx) { |
||||
const wallet = this._getWalletForAccount(address) |
||||
var privKey = wallet.getPrivateKey() |
||||
tx.sign(privKey) |
||||
return Promise.resolve(tx) |
||||
} |
||||
|
||||
// For eth_sign, we need to sign transactions:
|
||||
signMessage (withAccount, data) { |
||||
const wallet = this._getWalletForAccount(withAccount) |
||||
const message = ethUtil.removeHexPrefix(data) |
||||
var privKey = wallet.getPrivateKey() |
||||
var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey) |
||||
var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s)) |
||||
return Promise.resolve(rawMsgSig) |
||||
} |
||||
|
||||
exportAccount (address) { |
||||
const wallet = this._getWalletForAccount(address) |
||||
return Promise.resolve(wallet.getPrivateKey().toString('hex')) |
||||
} |
||||
|
||||
|
||||
/* PRIVATE METHODS */ |
||||
|
||||
_initFromMnemonic (mnemonic) { |
||||
this.mnemonic = mnemonic |
||||
const seed = bip39.mnemonicToSeed(mnemonic) |
||||
this.hdWallet = hdkey.fromMasterSeed(seed) |
||||
this.root = this.hdWallet.derivePath(hdPathString) |
||||
} |
||||
|
||||
|
||||
_getWalletForAccount (account) { |
||||
return this.wallets.find((w) => { |
||||
const address = w.getAddress().toString('hex') |
||||
return ((address === account) || (sigUtil.normalize(address) === account)) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
HdKeyring.type = type |
||||
module.exports = HdKeyring |
@ -0,0 +1,82 @@ |
||||
const EventEmitter = require('events').EventEmitter |
||||
const Wallet = require('ethereumjs-wallet') |
||||
const ethUtil = require('ethereumjs-util') |
||||
const type = 'Simple Key Pair' |
||||
const sigUtil = require('../lib/sig-util') |
||||
|
||||
class SimpleKeyring extends EventEmitter { |
||||
|
||||
/* PUBLIC METHODS */ |
||||
|
||||
constructor (opts) { |
||||
super() |
||||
this.type = type |
||||
this.opts = opts || {} |
||||
this.wallets = [] |
||||
} |
||||
|
||||
serialize () { |
||||
return Promise.resolve(this.wallets.map(w => w.getPrivateKey().toString('hex'))) |
||||
} |
||||
|
||||
deserialize (privateKeys = []) { |
||||
this.wallets = privateKeys.map((privateKey) => { |
||||
const stripped = ethUtil.stripHexPrefix(privateKey) |
||||
const buffer = new Buffer(stripped, 'hex') |
||||
const wallet = Wallet.fromPrivateKey(buffer) |
||||
return wallet |
||||
}) |
||||
return Promise.resolve() |
||||
} |
||||
|
||||
addAccounts (n = 1) { |
||||
var newWallets = [] |
||||
for (var i = 0; i < n; i++) { |
||||
newWallets.push(Wallet.generate()) |
||||
} |
||||
this.wallets = this.wallets.concat(newWallets) |
||||
const hexWallets = newWallets.map(w => ethUtil.bufferToHex(w.getAddress())) |
||||
return Promise.resolve(hexWallets) |
||||
} |
||||
|
||||
getAccounts () { |
||||
return Promise.resolve(this.wallets.map(w => ethUtil.bufferToHex(w.getAddress()))) |
||||
} |
||||
|
||||
// tx is an instance of the ethereumjs-transaction class.
|
||||
signTransaction (address, tx) { |
||||
const wallet = this._getWalletForAccount(address) |
||||
var privKey = wallet.getPrivateKey() |
||||
tx.sign(privKey) |
||||
return Promise.resolve(tx) |
||||
} |
||||
|
||||
// For eth_sign, we need to sign transactions:
|
||||
signMessage (withAccount, data) { |
||||
const wallet = this._getWalletForAccount(withAccount) |
||||
|
||||
const message = ethUtil.removeHexPrefix(data) |
||||
var privKey = wallet.getPrivateKey() |
||||
var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey) |
||||
var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s)) |
||||
return Promise.resolve(rawMsgSig) |
||||
} |
||||
|
||||
exportAccount (address) { |
||||
const wallet = this._getWalletForAccount(address) |
||||
return Promise.resolve(wallet.getPrivateKey().toString('hex')) |
||||
} |
||||
|
||||
|
||||
/* PRIVATE METHODS */ |
||||
|
||||
_getWalletForAccount (account) { |
||||
let wallet = this.wallets.find(w => ethUtil.bufferToHex(w.getAddress()) === account) |
||||
if (!wallet) throw new Error('Simple Keyring - Unable to find matching address.') |
||||
return wallet |
||||
} |
||||
|
||||
} |
||||
|
||||
SimpleKeyring.type = type |
||||
module.exports = SimpleKeyring |
@ -0,0 +1,146 @@ |
||||
/* Ethereum Store |
||||
* |
||||
* This module is responsible for tracking any number of accounts |
||||
* and caching their current balances & transaction counts. |
||||
* |
||||
* It also tracks transaction hashes, and checks their inclusion status |
||||
* on each new block. |
||||
*/ |
||||
|
||||
const EventEmitter = require('events').EventEmitter |
||||
const inherits = require('util').inherits |
||||
const async = require('async') |
||||
const clone = require('clone') |
||||
const EthQuery = require('eth-query') |
||||
|
||||
module.exports = EthereumStore |
||||
|
||||
|
||||
inherits(EthereumStore, EventEmitter) |
||||
function EthereumStore(engine) { |
||||
const self = this |
||||
EventEmitter.call(self) |
||||
self._currentState = { |
||||
accounts: {}, |
||||
transactions: {}, |
||||
} |
||||
self._query = new EthQuery(engine) |
||||
|
||||
engine.on('block', self._updateForBlock.bind(self)) |
||||
} |
||||
|
||||
//
|
||||
// public
|
||||
//
|
||||
|
||||
EthereumStore.prototype.getState = function () { |
||||
const self = this |
||||
return clone(self._currentState) |
||||
} |
||||
|
||||
EthereumStore.prototype.addAccount = function (address) { |
||||
const self = this |
||||
self._currentState.accounts[address] = {} |
||||
self._didUpdate() |
||||
if (!self.currentBlockNumber) return |
||||
self._updateAccount(address, () => { |
||||
self._didUpdate() |
||||
}) |
||||
} |
||||
|
||||
EthereumStore.prototype.removeAccount = function (address) { |
||||
const self = this |
||||
delete self._currentState.accounts[address] |
||||
self._didUpdate() |
||||
} |
||||
|
||||
EthereumStore.prototype.addTransaction = function (txHash) { |
||||
const self = this |
||||
self._currentState.transactions[txHash] = {} |
||||
self._didUpdate() |
||||
if (!self.currentBlockNumber) return |
||||
self._updateTransaction(self.currentBlockNumber, txHash, noop) |
||||
} |
||||
|
||||
EthereumStore.prototype.removeTransaction = function (address) { |
||||
const self = this |
||||
delete self._currentState.transactions[address] |
||||
self._didUpdate() |
||||
} |
||||
|
||||
|
||||
//
|
||||
// private
|
||||
//
|
||||
|
||||
EthereumStore.prototype._didUpdate = function () { |
||||
const self = this |
||||
var state = self.getState() |
||||
self.emit('update', state) |
||||
} |
||||
|
||||
EthereumStore.prototype._updateForBlock = function (block) { |
||||
const self = this |
||||
var blockNumber = '0x' + block.number.toString('hex') |
||||
self.currentBlockNumber = blockNumber |
||||
async.parallel([ |
||||
self._updateAccounts.bind(self), |
||||
self._updateTransactions.bind(self, blockNumber), |
||||
], function (err) { |
||||
if (err) return console.error(err) |
||||
self.emit('block', self.getState()) |
||||
self._didUpdate() |
||||
}) |
||||
} |
||||
|
||||
EthereumStore.prototype._updateAccounts = function (cb) { |
||||
var accountsState = this._currentState.accounts |
||||
var addresses = Object.keys(accountsState) |
||||
async.each(addresses, this._updateAccount.bind(this), cb) |
||||
} |
||||
|
||||
EthereumStore.prototype._updateAccount = function (address, cb) { |
||||
var accountsState = this._currentState.accounts |
||||
this.getAccount(address, function (err, result) { |
||||
if (err) return cb(err) |
||||
result.address = address |
||||
// only populate if the entry is still present
|
||||
if (accountsState[address]) { |
||||
accountsState[address] = result |
||||
} |
||||
cb(null, result) |
||||
}) |
||||
} |
||||
|
||||
EthereumStore.prototype.getAccount = function (address, cb) { |
||||
const query = this._query |
||||
async.parallel({ |
||||
balance: query.getBalance.bind(query, address), |
||||
nonce: query.getTransactionCount.bind(query, address), |
||||
code: query.getCode.bind(query, address), |
||||
}, cb) |
||||
} |
||||
|
||||
EthereumStore.prototype._updateTransactions = function (block, cb) { |
||||
const self = this |
||||
var transactionsState = self._currentState.transactions |
||||
var txHashes = Object.keys(transactionsState) |
||||
async.each(txHashes, self._updateTransaction.bind(self, block), cb) |
||||
} |
||||
|
||||
EthereumStore.prototype._updateTransaction = function (block, txHash, cb) { |
||||
const self = this |
||||
// would use the block here to determine how many confirmations the tx has
|
||||
var transactionsState = self._currentState.transactions |
||||
self._query.getTransaction(txHash, function (err, result) { |
||||
if (err) return cb(err) |
||||
// only populate if the entry is still present
|
||||
if (transactionsState[txHash]) { |
||||
transactionsState[txHash] = result |
||||
self._didUpdate() |
||||
} |
||||
cb(null, result) |
||||
}) |
||||
} |
||||
|
||||
function noop() {} |
@ -0,0 +1,80 @@ |
||||
const IdentityStore = require('./idStore') |
||||
const HdKeyring = require('../keyrings/hd') |
||||
const sigUtil = require('./sig-util') |
||||
const normalize = sigUtil.normalize |
||||
const denodeify = require('denodeify') |
||||
|
||||
module.exports = class IdentityStoreMigrator { |
||||
|
||||
constructor ({ configManager }) { |
||||
this.configManager = configManager |
||||
const hasOldVault = this.hasOldVault() |
||||
if (!hasOldVault) { |
||||
this.idStore = new IdentityStore({ configManager }) |
||||
} |
||||
} |
||||
|
||||
migratedVaultForPassword (password) { |
||||
const hasOldVault = this.hasOldVault() |
||||
const configManager = this.configManager |
||||
|
||||
if (!this.idStore) { |
||||
this.idStore = new IdentityStore({ configManager }) |
||||
} |
||||
|
||||
if (!hasOldVault) { |
||||
return Promise.resolve(null) |
||||
} |
||||
|
||||
const idStore = this.idStore |
||||
const submitPassword = denodeify(idStore.submitPassword.bind(idStore)) |
||||
|
||||
return submitPassword(password) |
||||
.then(() => { |
||||
const serialized = this.serializeVault() |
||||
return this.checkForLostAccounts(serialized) |
||||
}) |
||||
} |
||||
|
||||
serializeVault () { |
||||
const mnemonic = this.idStore._idmgmt.getSeed() |
||||
const numberOfAccounts = this.idStore._getAddresses().length |
||||
|
||||
return { |
||||
type: 'HD Key Tree', |
||||
data: { mnemonic, numberOfAccounts }, |
||||
} |
||||
} |
||||
|
||||
checkForLostAccounts (serialized) { |
||||
const hd = new HdKeyring() |
||||
return hd.deserialize(serialized.data) |
||||
.then((hexAccounts) => { |
||||
const newAccounts = hexAccounts.map(normalize) |
||||
const oldAccounts = this.idStore._getAddresses().map(normalize) |
||||
const lostAccounts = oldAccounts.reduce((result, account) => { |
||||
if (newAccounts.includes(account)) { |
||||
return result |
||||
} else { |
||||
result.push(account) |
||||
return result |
||||
} |
||||
}, []) |
||||
|
||||
return { |
||||
serialized, |
||||
lostAccounts: lostAccounts.map((address) => { |
||||
return { |
||||
address, |
||||
privateKey: this.idStore.exportAccount(address), |
||||
} |
||||
}), |
||||
} |
||||
}) |
||||
} |
||||
|
||||
hasOldVault () { |
||||
const wallet = this.configManager.getWallet() |
||||
return wallet |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
module.exports = function (promiseFn) { |
||||
return function () { |
||||
var args = [] |
||||
for (var i = 0; i < arguments.length - 1; i++) { |
||||
args.push(arguments[i]) |
||||
} |
||||
var cb = arguments[arguments.length - 1] |
||||
|
||||
const nodeified = promiseFn.apply(this, args) |
||||
|
||||
if (!nodeified) { |
||||
const methodName = String(promiseFn).split('(')[0] |
||||
throw new Error(`The ${methodName} did not return a Promise, but was nodeified.`) |
||||
} |
||||
nodeified.then(function (result) { |
||||
cb(null, result) |
||||
}) |
||||
.catch(function (reason) { |
||||
cb(reason) |
||||
}) |
||||
|
||||
return nodeified |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
const ethUtil = require('ethereumjs-util') |
||||
|
||||
module.exports = { |
||||
|
||||
concatSig: function (v, r, s) { |
||||
const rSig = ethUtil.fromSigned(r) |
||||
const sSig = ethUtil.fromSigned(s) |
||||
const vSig = ethUtil.bufferToInt(v) |
||||
const rStr = padWithZeroes(ethUtil.toUnsigned(rSig).toString('hex'), 64) |
||||
const sStr = padWithZeroes(ethUtil.toUnsigned(sSig).toString('hex'), 64) |
||||
const vStr = ethUtil.stripHexPrefix(ethUtil.intToHex(vSig)) |
||||
return ethUtil.addHexPrefix(rStr.concat(sStr, vStr)).toString('hex') |
||||
}, |
||||
|
||||
normalize: function (address) { |
||||
if (!address) return |
||||
return ethUtil.addHexPrefix(address.toLowerCase()) |
||||
}, |
||||
|
||||
} |
||||
|
||||
function padWithZeroes (number, length) { |
||||
var myString = '' + number |
||||
while (myString.length < length) { |
||||
myString = '0' + myString |
||||
} |
||||
return myString |
||||
} |
@ -0,0 +1,132 @@ |
||||
const async = require('async') |
||||
const EthQuery = require('eth-query') |
||||
const ethUtil = require('ethereumjs-util') |
||||
const Transaction = require('ethereumjs-tx') |
||||
const normalize = require('./sig-util').normalize |
||||
const BN = ethUtil.BN |
||||
|
||||
/* |
||||
tx-utils are utility methods for Transaction manager |
||||
its passed a provider and that is passed to ethquery |
||||
and used to do things like calculate gas of a tx. |
||||
*/ |
||||
|
||||
module.exports = class txProviderUtils { |
||||
constructor (provider) { |
||||
this.provider = provider |
||||
this.query = new EthQuery(provider) |
||||
} |
||||
|
||||
analyzeGasUsage (txData, cb) { |
||||
var self = this |
||||
this.query.getBlockByNumber('latest', true, (err, block) => { |
||||
if (err) return cb(err) |
||||
async.waterfall([ |
||||
self.estimateTxGas.bind(self, txData, block.gasLimit), |
||||
self.setTxGas.bind(self, txData, block.gasLimit), |
||||
], cb) |
||||
}) |
||||
} |
||||
|
||||
estimateTxGas (txData, blockGasLimitHex, cb) { |
||||
const txParams = txData.txParams |
||||
// check if gasLimit is already specified
|
||||
txData.gasLimitSpecified = Boolean(txParams.gas) |
||||
// if not, fallback to block gasLimit
|
||||
if (!txData.gasLimitSpecified) { |
||||
txParams.gas = blockGasLimitHex |
||||
} |
||||
// run tx, see if it will OOG
|
||||
this.query.estimateGas(txParams, cb) |
||||
} |
||||
|
||||
setTxGas (txData, blockGasLimitHex, estimatedGasHex, cb) { |
||||
txData.estimatedGas = estimatedGasHex |
||||
const txParams = txData.txParams |
||||
|
||||
// if gasLimit was specified and doesnt OOG,
|
||||
// use original specified amount
|
||||
if (txData.gasLimitSpecified) { |
||||
txData.estimatedGas = txParams.gas |
||||
cb() |
||||
return |
||||
} |
||||
// if gasLimit not originally specified,
|
||||
// try adding an additional gas buffer to our estimation for safety
|
||||
const estimatedGasBn = new BN(ethUtil.stripHexPrefix(txData.estimatedGas), 16) |
||||
const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(blockGasLimitHex), 16) |
||||
const estimationWithBuffer = new BN(this.addGasBuffer(estimatedGasBn), 16) |
||||
// added gas buffer is too high
|
||||
if (estimationWithBuffer.gt(blockGasLimitBn)) { |
||||
txParams.gas = txData.estimatedGas |
||||
// added gas buffer is safe
|
||||
} else { |
||||
const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer) |
||||
txParams.gas = gasWithBufferHex |
||||
} |
||||
cb() |
||||
return |
||||
} |
||||
|
||||
addGasBuffer (gas) { |
||||
const gasBuffer = new BN('100000', 10) |
||||
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) |
||||
const correct = bnGas.add(gasBuffer) |
||||
return ethUtil.addHexPrefix(correct.toString(16)) |
||||
} |
||||
|
||||
fillInTxParams (txParams, cb) { |
||||
let fromAddress = txParams.from |
||||
let reqs = {} |
||||
|
||||
if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb) |
||||
if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb) |
||||
if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb) |
||||
|
||||
async.parallel(reqs, function(err, result) { |
||||
if (err) return cb(err) |
||||
// write results to txParams obj
|
||||
Object.assign(txParams, result) |
||||
cb() |
||||
}) |
||||
} |
||||
|
||||
// builds ethTx from txParams object
|
||||
buildEthTxFromParams (txParams, gasMultiplier = 1) { |
||||
// apply gas multiplyer
|
||||
let gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16) |
||||
// multiply and divide by 100 so as to add percision to integer mul
|
||||
gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10)) |
||||
txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber()) |
||||
// normalize values
|
||||
txParams.to = normalize(txParams.to) |
||||
txParams.from = normalize(txParams.from) |
||||
txParams.value = normalize(txParams.value) |
||||
txParams.data = normalize(txParams.data) |
||||
txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas) |
||||
txParams.nonce = normalize(txParams.nonce) |
||||
// build ethTx
|
||||
const ethTx = new Transaction(txParams) |
||||
return ethTx |
||||
} |
||||
|
||||
publishTransaction (rawTx, cb) { |
||||
this.query.sendRawTransaction(rawTx, cb) |
||||
} |
||||
|
||||
validateTxParams (txParams, cb) { |
||||
if (('value' in txParams) && txParams.value.indexOf('-') === 0) { |
||||
cb(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)) |
||||
} else { |
||||
cb() |
||||
} |
||||
} |
||||
|
||||
|
||||
} |
||||
|
||||
// util
|
||||
|
||||
function isUndef(value) { |
||||
return value === undefined |
||||
} |
@ -0,0 +1,362 @@ |
||||
const EventEmitter = require('events') |
||||
const async = require('async') |
||||
const extend = require('xtend') |
||||
const Semaphore = require('semaphore') |
||||
const ethUtil = require('ethereumjs-util') |
||||
const BN = require('ethereumjs-util').BN |
||||
const TxProviderUtil = require('./lib/tx-utils') |
||||
const createId = require('./lib/random-id') |
||||
|
||||
module.exports = class TransactionManager extends EventEmitter { |
||||
constructor (opts) { |
||||
super() |
||||
this.txList = opts.txList || [] |
||||
this._setTxList = opts.setTxList |
||||
this.txHistoryLimit = opts.txHistoryLimit |
||||
this.getSelectedAccount = opts.getSelectedAccount |
||||
this.provider = opts.provider |
||||
this.blockTracker = opts.blockTracker |
||||
this.txProviderUtils = new TxProviderUtil(this.provider) |
||||
this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) |
||||
this.getGasMultiplier = opts.getGasMultiplier |
||||
this.getNetwork = opts.getNetwork |
||||
this.signEthTx = opts.signTransaction |
||||
this.nonceLock = Semaphore(1) |
||||
} |
||||
|
||||
getState () { |
||||
var selectedAccount = this.getSelectedAccount() |
||||
return { |
||||
transactions: this.getTxList(), |
||||
unconfTxs: this.getUnapprovedTxList(), |
||||
selectedAccountTxList: this.getFilteredTxList({metamaskNetworkId: this.getNetwork(), from: selectedAccount}), |
||||
} |
||||
} |
||||
|
||||
// Returns the tx list
|
||||
getTxList () { |
||||
let network = this.getNetwork() |
||||
return this.txList.filter(txMeta => txMeta.metamaskNetworkId === network) |
||||
} |
||||
|
||||
// Adds a tx to the txlist
|
||||
addTx (txMeta) { |
||||
var txList = this.getTxList() |
||||
var txHistoryLimit = this.txHistoryLimit |
||||
|
||||
// checks if the length of th tx history is
|
||||
// longer then desired persistence limit
|
||||
// and then if it is removes only confirmed
|
||||
// or rejected tx's.
|
||||
// not tx's that are pending or unapproved
|
||||
if (txList.length > txHistoryLimit - 1) { |
||||
var index = txList.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') |
||||
txList.splice(index, 1) |
||||
} |
||||
txList.push(txMeta) |
||||
|
||||
this._saveTxList(txList) |
||||
this.once(`${txMeta.id}:signed`, function (txId) { |
||||
this.removeAllListeners(`${txMeta.id}:rejected`) |
||||
}) |
||||
this.once(`${txMeta.id}:rejected`, function (txId) { |
||||
this.removeAllListeners(`${txMeta.id}:signed`) |
||||
}) |
||||
|
||||
this.emit('updateBadge') |
||||
this.emit(`${txMeta.id}:unapproved`, txMeta) |
||||
} |
||||
|
||||
// gets tx by Id and returns it
|
||||
getTx (txId, cb) { |
||||
var txList = this.getTxList() |
||||
var txMeta = txList.find(txData => txData.id === txId) |
||||
return cb ? cb(txMeta) : txMeta |
||||
} |
||||
|
||||
//
|
||||
updateTx (txMeta) { |
||||
var txId = txMeta.id |
||||
var txList = this.getTxList() |
||||
var index = txList.findIndex(txData => txData.id === txId) |
||||
txList[index] = txMeta |
||||
this._saveTxList(txList) |
||||
this.emit('update') |
||||
} |
||||
|
||||
get unapprovedTxCount () { |
||||
return Object.keys(this.getUnapprovedTxList()).length |
||||
} |
||||
|
||||
get pendingTxCount () { |
||||
return this.getTxsByMetaData('status', 'signed').length |
||||
} |
||||
|
||||
addUnapprovedTransaction (txParams, done) { |
||||
let txMeta |
||||
async.waterfall([ |
||||
// validate
|
||||
(cb) => this.txProviderUtils.validateTxParams(txParams, cb), |
||||
// prepare txMeta
|
||||
(cb) => { |
||||
// create txMeta obj with parameters and meta data
|
||||
let time = (new Date()).getTime() |
||||
let txId = createId() |
||||
txParams.metamaskId = txId |
||||
txParams.metamaskNetworkId = this.getNetwork() |
||||
txMeta = { |
||||
id: txId, |
||||
time: time, |
||||
status: 'unapproved', |
||||
gasMultiplier: this.getGasMultiplier() || 1, |
||||
metamaskNetworkId: this.getNetwork(), |
||||
txParams: txParams, |
||||
} |
||||
// calculate metadata for tx
|
||||
this.txProviderUtils.analyzeGasUsage(txMeta, cb) |
||||
}, |
||||
// save txMeta
|
||||
(cb) => { |
||||
this.addTx(txMeta) |
||||
this.setMaxTxCostAndFee(txMeta) |
||||
cb(null, txMeta) |
||||
}, |
||||
], done) |
||||
} |
||||
|
||||
setMaxTxCostAndFee (txMeta) { |
||||
var txParams = txMeta.txParams |
||||
var gasMultiplier = txMeta.gasMultiplier |
||||
var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txMeta.estimatedGas), 16) |
||||
var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16) |
||||
gasPrice = gasPrice.mul(new BN(gasMultiplier * 100), 10).div(new BN(100, 10)) |
||||
var txFee = gasCost.mul(gasPrice) |
||||
var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16) |
||||
var maxCost = txValue.add(txFee) |
||||
txMeta.txFee = txFee |
||||
txMeta.txValue = txValue |
||||
txMeta.maxCost = maxCost |
||||
this.updateTx(txMeta) |
||||
} |
||||
|
||||
getUnapprovedTxList () { |
||||
var txList = this.getTxList() |
||||
return txList.filter((txMeta) => txMeta.status === 'unapproved') |
||||
.reduce((result, tx) => { |
||||
result[tx.id] = tx |
||||
return result |
||||
}, {}) |
||||
} |
||||
|
||||
approveTransaction (txId, cb = warn) { |
||||
const self = this |
||||
// approve
|
||||
self.setTxStatusApproved(txId) |
||||
// only allow one tx at a time for atomic nonce usage
|
||||
self.nonceLock.take(() => { |
||||
// begin signature process
|
||||
async.waterfall([ |
||||
(cb) => self.fillInTxParams(txId, cb), |
||||
(cb) => self.signTransaction(txId, cb), |
||||
(rawTx, cb) => self.publishTransaction(txId, rawTx, cb), |
||||
], (err) => { |
||||
self.nonceLock.leave() |
||||
if (err) { |
||||
this.setTxStatusFailed(txId) |
||||
return cb(err) |
||||
} |
||||
cb() |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
cancelTransaction (txId, cb = warn) { |
||||
this.setTxStatusRejected(txId) |
||||
cb() |
||||
} |
||||
|
||||
fillInTxParams (txId, cb) { |
||||
let txMeta = this.getTx(txId) |
||||
this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { |
||||
if (err) return cb(err) |
||||
this.updateTx(txMeta) |
||||
cb() |
||||
}) |
||||
} |
||||
|
||||
signTransaction (txId, cb) { |
||||
let txMeta = this.getTx(txId) |
||||
let txParams = txMeta.txParams |
||||
let fromAddress = txParams.from |
||||
let ethTx = this.txProviderUtils.buildEthTxFromParams(txParams, txMeta.gasMultiplier) |
||||
this.signEthTx(ethTx, fromAddress).then(() => { |
||||
this.updateTxAsSigned(txMeta.id, ethTx) |
||||
cb(null, ethUtil.bufferToHex(ethTx.serialize())) |
||||
}).catch((err) => { |
||||
cb(err) |
||||
}) |
||||
} |
||||
|
||||
publishTransaction (txId, rawTx, cb) { |
||||
this.txProviderUtils.publishTransaction(rawTx, (err) => { |
||||
if (err) return cb(err) |
||||
this.setTxStatusSubmitted(txId) |
||||
cb() |
||||
}) |
||||
} |
||||
|
||||
// receives a signed tx object and updates the tx hash
|
||||
updateTxAsSigned (txId, ethTx) { |
||||
// Add the tx hash to the persisted meta-tx object
|
||||
let txHash = ethUtil.bufferToHex(ethTx.hash()) |
||||
let txMeta = this.getTx(txId) |
||||
txMeta.hash = txHash |
||||
this.updateTx(txMeta) |
||||
this.setTxStatusSigned(txMeta.id) |
||||
} |
||||
|
||||
/* |
||||
Takes an object of fields to search for eg: |
||||
var thingsToLookFor = { |
||||
to: '0x0..', |
||||
from: '0x0..', |
||||
status: 'signed', |
||||
} |
||||
and returns a list of tx with all |
||||
options matching |
||||
|
||||
this is for things like filtering a the tx list |
||||
for only tx's from 1 account |
||||
or for filltering for all txs from one account |
||||
and that have been 'confirmed' |
||||
*/ |
||||
getFilteredTxList (opts) { |
||||
var filteredTxList |
||||
Object.keys(opts).forEach((key) => { |
||||
filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) |
||||
}) |
||||
return filteredTxList |
||||
} |
||||
|
||||
getTxsByMetaData (key, value, txList = this.getTxList()) { |
||||
return txList.filter((txMeta) => { |
||||
if (key in txMeta.txParams) { |
||||
return txMeta.txParams[key] === value |
||||
} else { |
||||
return txMeta[key] === value |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// STATUS METHODS
|
||||
// get::set status
|
||||
|
||||
// should return the status of the tx.
|
||||
getTxStatus (txId) { |
||||
const txMeta = this.getTx(txId) |
||||
return txMeta.status |
||||
} |
||||
|
||||
// should update the status of the tx to 'rejected'.
|
||||
setTxStatusRejected (txId) { |
||||
this._setTxStatus(txId, 'rejected') |
||||
} |
||||
|
||||
// should update the status of the tx to 'approved'.
|
||||
setTxStatusApproved (txId) { |
||||
this._setTxStatus(txId, 'approved') |
||||
} |
||||
|
||||
// should update the status of the tx to 'signed'.
|
||||
setTxStatusSigned (txId) { |
||||
this._setTxStatus(txId, 'signed') |
||||
} |
||||
|
||||
// should update the status of the tx to 'submitted'.
|
||||
setTxStatusSubmitted (txId) { |
||||
this._setTxStatus(txId, 'submitted') |
||||
} |
||||
|
||||
// should update the status of the tx to 'confirmed'.
|
||||
setTxStatusConfirmed (txId) { |
||||
this._setTxStatus(txId, 'confirmed') |
||||
} |
||||
|
||||
setTxStatusFailed (txId) { |
||||
this._setTxStatus(txId, 'failed') |
||||
} |
||||
|
||||
// merges txParams obj onto txData.txParams
|
||||
// use extend to ensure that all fields are filled
|
||||
updateTxParams (txId, txParams) { |
||||
var txMeta = this.getTx(txId) |
||||
txMeta.txParams = extend(txMeta.txParams, txParams) |
||||
this.updateTx(txMeta) |
||||
} |
||||
|
||||
// checks if a signed tx is in a block and
|
||||
// if included sets the tx status as 'confirmed'
|
||||
checkForTxInBlock () { |
||||
var signedTxList = this.getFilteredTxList({status: 'signed'}) |
||||
if (!signedTxList.length) return |
||||
signedTxList.forEach((txMeta) => { |
||||
var txHash = txMeta.hash |
||||
var txId = txMeta.id |
||||
if (!txHash) { |
||||
txMeta.err = { |
||||
errCode: 'No hash was provided', |
||||
message: 'We had an error while submitting this transaction, please try again.', |
||||
} |
||||
this.updateTx(txMeta) |
||||
return this.setTxStatusFailed(txId) |
||||
} |
||||
this.txProviderUtils.query.getTransactionByHash(txHash, (err, txParams) => { |
||||
if (err || !txParams) { |
||||
if (!txParams) return |
||||
txMeta.err = { |
||||
isWarning: true, |
||||
errorCode: err, |
||||
message: 'There was a problem loading this transaction.', |
||||
} |
||||
this.updateTx(txMeta) |
||||
return console.error(err) |
||||
} |
||||
if (txParams.blockNumber) { |
||||
this.setTxStatusConfirmed(txId) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
// PRIVATE METHODS
|
||||
|
||||
// Should find the tx in the tx list and
|
||||
// update it.
|
||||
// should set the status in txData
|
||||
// - `'unapproved'` the user has not responded
|
||||
// - `'rejected'` the user has responded no!
|
||||
// - `'approved'` the user has approved the tx
|
||||
// - `'signed'` the tx is signed
|
||||
// - `'submitted'` the tx is sent to a server
|
||||
// - `'confirmed'` the tx has been included in a block.
|
||||
_setTxStatus (txId, status) { |
||||
var txMeta = this.getTx(txId) |
||||
txMeta.status = status |
||||
this.emit(`${txMeta.id}:${status}`, txId) |
||||
if (status === 'submitted' || status === 'rejected') { |
||||
this.emit(`${txMeta.id}:finished`, status) |
||||
} |
||||
this.updateTx(txMeta) |
||||
this.emit('updateBadge') |
||||
} |
||||
|
||||
// Saves the new/updated txList.
|
||||
// Function is intended only for internal use
|
||||
_saveTxList (txList) { |
||||
this.txList = txList |
||||
this._setTxList(txList) |
||||
} |
||||
} |
||||
|
||||
|
||||
const warn = () => console.warn('warn was used no cb provided') |
File diff suppressed because one or more lines are too long
@ -0,0 +1,126 @@ |
||||
{ |
||||
"metamask": { |
||||
"isInitialized": true, |
||||
"isUnlocked": true, |
||||
"rpcTarget": "https://rawtestrpc.metamask.io/", |
||||
"identities": { |
||||
"0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9": { |
||||
"address": "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9", |
||||
"name": "Account 1" |
||||
}, |
||||
"0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4": { |
||||
"address": "0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4", |
||||
"name": "Account 2" |
||||
}, |
||||
"0x1acfb961c5a8268eac8e09d6241a26cbeff42241": { |
||||
"address": "0x1acfb961c5a8268eac8e09d6241a26cbeff42241", |
||||
"name": "Account 3" |
||||
}, |
||||
"0xe15d894becb0354c501ae69429b05143679f39e0": { |
||||
"address": "0xe15d894becb0354c501ae69429b05143679f39e0", |
||||
"name": "Account 4" |
||||
}, |
||||
"0x87658c15aefe7448008a28513a11b6b130ef4cd0": { |
||||
"address": "0x87658c15aefe7448008a28513a11b6b130ef4cd0", |
||||
"name": "Account 5" |
||||
}, |
||||
"0xaa25854c0379e53c957ac9382e720c577fa31fd5": { |
||||
"address": "0xaa25854c0379e53c957ac9382e720c577fa31fd5", |
||||
"name": "Account 6" |
||||
} |
||||
}, |
||||
"unconfTxs": {}, |
||||
"currentFiat": "USD", |
||||
"conversionRate": 0, |
||||
"conversionDate": "N/A", |
||||
"noActiveNotices": true, |
||||
"network": "3", |
||||
"accounts": { |
||||
"0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9": { |
||||
"code": "0x", |
||||
"balance": "0x11f646fe14c9c000", |
||||
"nonce": "0x3", |
||||
"address": "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9" |
||||
}, |
||||
"0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4": { |
||||
"code": "0x", |
||||
"balance": "0x0", |
||||
"nonce": "0x0", |
||||
"address": "0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4" |
||||
}, |
||||
"0x1acfb961c5a8268eac8e09d6241a26cbeff42241": { |
||||
"code": "0x", |
||||
"balance": "0x0", |
||||
"nonce": "0x0", |
||||
"address": "0x1acfb961c5a8268eac8e09d6241a26cbeff42241" |
||||
}, |
||||
"0xe15d894becb0354c501ae69429b05143679f39e0": { |
||||
"code": "0x", |
||||
"balance": "0x0", |
||||
"nonce": "0x0", |
||||
"address": "0xe15d894becb0354c501ae69429b05143679f39e0" |
||||
}, |
||||
"0x87658c15aefe7448008a28513a11b6b130ef4cd0": { |
||||
"code": "0x", |
||||
"balance": "0x0", |
||||
"nonce": "0x0", |
||||
"address": "0x87658c15aefe7448008a28513a11b6b130ef4cd0" |
||||
}, |
||||
"0xaa25854c0379e53c957ac9382e720c577fa31fd5": { |
||||
"code": "0x", |
||||
"balance": "0x0", |
||||
"nonce": "0x0", |
||||
"address": "0xaa25854c0379e53c957ac9382e720c577fa31fd5" |
||||
} |
||||
}, |
||||
"transactions": [], |
||||
"provider": { |
||||
"type": "testnet" |
||||
}, |
||||
"selectedAccount": "0x87658c15aefe7448008a28513a11b6b130ef4cd0", |
||||
"isDisclaimerConfirmed": true, |
||||
"unconfMsgs": {}, |
||||
"messages": [], |
||||
"shapeShiftTxList": [], |
||||
"keyringTypes": [ |
||||
"Simple Key Pair", |
||||
"HD Key Tree" |
||||
], |
||||
"keyrings": [ |
||||
{ |
||||
"type": "HD Key Tree", |
||||
"accounts": [ |
||||
"ac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9", |
||||
"d7c0cd9e7d2701c710d64fc492c7086679bdf7b4", |
||||
"1acfb961c5a8268eac8e09d6241a26cbeff42241" |
||||
] |
||||
}, |
||||
{ |
||||
"type": "Simple Key Pair", |
||||
"accounts": [ |
||||
"e15d894becb0354c501ae69429b05143679f39e0", |
||||
"87658c15aefe7448008a28513a11b6b130ef4cd0", |
||||
"aa25854c0379e53c957ac9382e720c577fa31fd5" |
||||
] |
||||
} |
||||
], |
||||
"lostAccounts": [] |
||||
}, |
||||
"appState": { |
||||
"menuOpen": false, |
||||
"currentView": { |
||||
"name": "accounts" |
||||
}, |
||||
"accountDetail": { |
||||
"subview": "transactions", |
||||
"accountExport": "none", |
||||
"privateKey": "" |
||||
}, |
||||
"transForward": true, |
||||
"isLoading": false, |
||||
"warning": null, |
||||
"scrollToBottom": false, |
||||
"forgottenPassword": false |
||||
}, |
||||
"identities": {} |
||||
} |
@ -0,0 +1,91 @@ |
||||
{ |
||||
"metamask": { |
||||
"currentFiat": "USD", |
||||
"lostAccounts": [ |
||||
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", |
||||
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b" |
||||
], |
||||
"conversionRate": 11.06608791, |
||||
"conversionDate": 1470421024, |
||||
"isInitialized": true, |
||||
"isUnlocked": true, |
||||
"currentDomain": "example.com", |
||||
"rpcTarget": "https://rawtestrpc.metamask.io/", |
||||
"identities": { |
||||
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { |
||||
"name": "Wallet 1", |
||||
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", |
||||
"mayBeFauceting": false |
||||
}, |
||||
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": { |
||||
"name": "Wallet 2", |
||||
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", |
||||
"mayBeFauceting": false |
||||
}, |
||||
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": { |
||||
"name": "Wallet 3", |
||||
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823", |
||||
"mayBeFauceting": false |
||||
}, |
||||
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": { |
||||
"name": "Wallet 4", |
||||
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69", |
||||
"mayBeFauceting": false |
||||
} |
||||
}, |
||||
"unconfTxs": {}, |
||||
"accounts": { |
||||
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { |
||||
"code": "0x", |
||||
"balance": "0x100000000000", |
||||
"nonce": "0x0", |
||||
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" |
||||
}, |
||||
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": { |
||||
"code": "0x", |
||||
"nonce": "0x0", |
||||
"balance": "0x100000000000", |
||||
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b" |
||||
}, |
||||
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": { |
||||
"code": "0x", |
||||
"nonce": "0x0", |
||||
"balance": "0x100000000000", |
||||
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823" |
||||
}, |
||||
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": { |
||||
"code": "0x", |
||||
"balance": "0x0", |
||||
"nonce": "0x0", |
||||
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69" |
||||
} |
||||
}, |
||||
"transactions": [], |
||||
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", |
||||
"network": "2", |
||||
"seedWords": null, |
||||
"isDisclaimerConfirmed": true, |
||||
"unconfMsgs": {}, |
||||
"messages": [], |
||||
"provider": { |
||||
"type": "testnet" |
||||
}, |
||||
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" |
||||
}, |
||||
"appState": { |
||||
"menuOpen": false, |
||||
"currentView": { |
||||
"name": "accountDetail", |
||||
"detailView": null, |
||||
"context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" |
||||
}, |
||||
"accountDetail": { |
||||
"subview": "transactions" |
||||
}, |
||||
"currentDomain": "127.0.0.1:9966", |
||||
"transForward": true, |
||||
"isLoading": false, |
||||
"warning": null |
||||
}, |
||||
"identities": {} |
||||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +1 @@ |
||||
{"metamask":{"isInitialized":true,"isUnlocked":true,"currentDomain":"example.com","rpcTarget":"https://rawtestrpc.metamask.io/","identities":{"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825":{"name":"Wallet 1","address":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","mayBeFauceting":false},"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb":{"name":"Wallet 2","address":"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb","mayBeFauceting":false},"0x2f8d4a878cfa04a6e60d46362f5644deab66572d":{"name":"Wallet 3","address":"0x2f8d4a878cfa04a6e60d46362f5644deab66572d","mayBeFauceting":false}},"unconfTxs":{"1467868023090690":{"id":1467868023090690,"txParams":{"data":"0xa9059cbb0000000000000000000000008deb4d106090c3eb8f1950f727e87c4f884fb06f0000000000000000000000000000000000000000000000000000000000000064","from":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","value":"0x16345785d8a0000","to":"0xbeb0ed3034c4155f3d16a64a5c5e7c8d4ea9e9c9","origin":"MetaMask","metamaskId":1467868023090690,"metamaskNetworkId":"2"},"time":1467868023090,"status":"unconfirmed","containsDelegateCall":false}},"accounts":{"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825":{"code":"0x","balance":"0x38326dc32cf80800","nonce":"0x10000c","address":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"},"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb":{"code":"0x","balance":"0x15e578bd8e9c8000","nonce":"0x100000","address":"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"},"0x2f8d4a878cfa04a6e60d46362f5644deab66572d":{"code":"0x","nonce":"0x100000","balance":"0x2386f26fc10000","address":"0x2f8d4a878cfa04a6e60d46362f5644deab66572d"}},"transactions":[],"selectedAddress":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","network":"2","seedWords":null,"isConfirmed":true,"unconfMsgs":{},"messages":[],"provider":{"type":"testnet"},"selectedAccount":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"},"appState":{"menuOpen":false,"currentView":{"name":"confTx","context":0},"accountDetail":{"subview":"transactions"},"currentDomain":"extensions","transForward":true,"isLoading":false,"warning":null},"identities":{}} |
||||
{"metamask":{"isInitialized":true,"isUnlocked":true,"currentDomain":"example.com","rpcTarget":"https://rawtestrpc.metamask.io/","identities":{"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825":{"name":"Wallet 1","address":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","mayBeFauceting":false},"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb":{"name":"Wallet 2","address":"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb","mayBeFauceting":false},"0x2f8d4a878cfa04a6e60d46362f5644deab66572d":{"name":"Wallet 3","address":"0x2f8d4a878cfa04a6e60d46362f5644deab66572d","mayBeFauceting":false}},"unconfTxs":{"1467868023090690":{"id":1467868023090690,"txParams":{"data":"0xa9059cbb0000000000000000000000008deb4d106090c3eb8f1950f727e87c4f884fb06f0000000000000000000000000000000000000000000000000000000000000064","from":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","value":"0x16345785d8a0000","to":"0xbeb0ed3034c4155f3d16a64a5c5e7c8d4ea9e9c9","origin":"MetaMask","metamaskId":1467868023090690,"metamaskNetworkId":"2"},"time":1467868023090,"status":"unconfirmed","containsDelegateCall":false}},"accounts":{"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825":{"code":"0x","balance":"0x38326dc32cf80800","nonce":"0x10000c","address":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"},"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb":{"code":"0x","balance":"0x15e578bd8e9c8000","nonce":"0x100000","address":"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"},"0x2f8d4a878cfa04a6e60d46362f5644deab66572d":{"code":"0x","nonce":"0x100000","balance":"0x2386f26fc10000","address":"0x2f8d4a878cfa04a6e60d46362f5644deab66572d"}},"transactions":[],"selectedAccount":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","network":"2","seedWords":null,"isDisclaimerConfirmed":true,"unconfMsgs":{},"messages":[],"provider":{"type":"testnet"},"selectedAccount":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"},"appState":{"menuOpen":false,"currentView":{"name":"confTx","context":0},"accountDetail":{"subview":"transactions"},"currentDomain":"extensions","transForward":true,"isLoading":false,"warning":null},"identities":{}} |
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,86 @@ |
||||
{ |
||||
"metamask": { |
||||
"isInitialized": true, |
||||
"isUnlocked": true, |
||||
"rpcTarget": "https://rawtestrpc.metamask.io/", |
||||
"identities": { |
||||
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { |
||||
"name": "Account 1", |
||||
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", |
||||
"mayBeFauceting": false |
||||
}, |
||||
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { |
||||
"name": "Account 2", |
||||
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", |
||||
"mayBeFauceting": false |
||||
} |
||||
}, |
||||
"unconfTxs": {}, |
||||
"currentFiat": "USD", |
||||
"conversionRate": 9.52855776, |
||||
"conversionDate": 1479756513, |
||||
"accounts": { |
||||
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { |
||||
"balance": "0x0", |
||||
"nonce": "0x0", |
||||
"code": "0x0", |
||||
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" |
||||
}, |
||||
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { |
||||
"balance": "0x0", |
||||
"nonce": "0x0", |
||||
"code": "0x0", |
||||
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb" |
||||
} |
||||
}, |
||||
"transactions": [ |
||||
{ |
||||
"id": 5551995700357153, |
||||
"txParams": { |
||||
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", |
||||
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63", |
||||
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb000000000000000000000000000000000000000000000000000000000000000a", |
||||
"metamaskId": 5551995700357153, |
||||
"metamaskNetworkId": "1479490588308" |
||||
}, |
||||
"time": 1479498745949, |
||||
"status": "confirmed", |
||||
"gasMultiplier": 1, |
||||
"metamaskNetworkId": "1479490588308", |
||||
"containsDelegateCall": true, |
||||
"estimatedGas": "0x24b33", |
||||
"hash": "0xad609a6931f54a575ad71222ffc27cd6746017106d5b89f4ad300b37b273f8ac" |
||||
} |
||||
], |
||||
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", |
||||
"network": "1479753732793", |
||||
"isConfirmed": true, |
||||
"isEthConfirmed": true, |
||||
"unconfMsgs": {}, |
||||
"messages": [], |
||||
"shapeShiftTxList": [], |
||||
"gasMultiplier": false, |
||||
"provider": { |
||||
"type": "rpc", |
||||
"rpcTarget": "http://localhost:8545" |
||||
}, |
||||
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", |
||||
"isDisclaimerConfirmed": true |
||||
}, |
||||
"appState": { |
||||
"menuOpen": false, |
||||
"currentView": { |
||||
"name": "accountDetail" |
||||
}, |
||||
"accountDetail": { |
||||
"subview": "transactions", |
||||
"accountExport": "none", |
||||
"privateKey": "" |
||||
}, |
||||
"transForward": false, |
||||
"isLoading": false, |
||||
"warning": null, |
||||
"scrollToBottom": false |
||||
}, |
||||
"identities": {} |
||||
} |
@ -0,0 +1,31 @@ |
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<title>MetaMask</title> |
||||
|
||||
<script> |
||||
window.METAMASK_DEBUG = true |
||||
window.TEST_MODE = true |
||||
</script> |
||||
</head> |
||||
<body> |
||||
|
||||
<!-- app content --> |
||||
<div id="app-content" style="height: 100%"></div> |
||||
<script src="./bundle.js" type="text/javascript" charset="utf-8"></script> |
||||
|
||||
</body> |
||||
|
||||
<style> |
||||
html, body, #app-content, .super-dev-container { |
||||
height: 100%; |
||||
width: 100%; |
||||
position: relative; |
||||
background: white; |
||||
} |
||||
.mock-app-root { |
||||
background: #F7F7F7; |
||||
} |
||||
</style> |
||||
</html> |
@ -0,0 +1,188 @@ |
||||
https://hackmd.io/JwIwDMDGKQZgtAFgKZjEgbARhPAhgKxZbwAcA7LAWOQCaKEgFA==?edit |
||||
|
||||
Subscribablez(initState) |
||||
.subscribe() |
||||
.emitUpdate(newState) |
||||
//.getState() |
||||
|
||||
|
||||
var initState = fromDisk() |
||||
ReduxStore(reducer, initState) |
||||
.reduce(action) -> .emitUpdate() |
||||
|
||||
ReduxStore.subscribe(toDisk) |
||||
|
||||
|
||||
### KeyChainManager / idStore 2.0 (maybe just in MetaMaskController) |
||||
keychains: [] |
||||
getAllAccounts(cb) |
||||
getAllKeychainViewStates(cb) -> returns [ KeyChainViewState] |
||||
|
||||
#### Old idStore external methods, for feature parity: |
||||
|
||||
- init(configManager) |
||||
- setStore(ethStore) |
||||
- getState() |
||||
- getSelectedAddres() |
||||
- setSelectedAddress() |
||||
- createNewVault() |
||||
- recoverFromSeed() |
||||
- submitPassword() |
||||
- approveTransaction() |
||||
- cancelTransaction() |
||||
- addUnconfirmedMessage(msgParams, cb) |
||||
- signMessage() |
||||
- cancelMessage() |
||||
- setLocked() |
||||
- clearSeedWordCache() |
||||
- exportAccount() |
||||
- revealAccount() |
||||
- saveAccountLabel() |
||||
- tryPassword() |
||||
- recoverSeed() |
||||
- getNetwork() |
||||
|
||||
##### Of those methods |
||||
|
||||
Where they should end up: |
||||
|
||||
##### MetaMaskController |
||||
|
||||
- getNetwork() |
||||
|
||||
##### KeyChainManager |
||||
|
||||
- init(configManager) |
||||
- setStore(ethStore) |
||||
- getState() // Deprecate for unidirectional flow |
||||
- on('update', cb) |
||||
- createNewVault(password) |
||||
- getSelectedAddres() |
||||
- setSelectedAddress() |
||||
- submitPassword() |
||||
- tryPassword() |
||||
- approveTransaction() |
||||
- cancelTransaction() |
||||
- signMessage() |
||||
- cancelMessage() |
||||
- setLocked() |
||||
- exportAccount() |
||||
|
||||
##### Bip44 KeyChain |
||||
|
||||
- getState() // Deprecate for unidirectional flow |
||||
- on('update', cb) |
||||
|
||||
If we adopt a ReactStore style unidirectional action dispatching data flow, these methods will be unified under a `dispatch` method, and rather than having a cb will emit an update to the UI: |
||||
|
||||
- createNewKeyChain(entropy) |
||||
- recoverFromSeed() |
||||
- approveTransaction() |
||||
- signMessage() |
||||
- clearSeedWordCache() |
||||
- exportAccount() |
||||
- revealAccount() |
||||
- saveAccountLabel() |
||||
- recoverSeed() |
||||
|
||||
Additional methods, new to this: |
||||
- serialize() |
||||
- Returns pojo with optional `secret` key whose contents will be encrypted with the users' password and salt when written to disk. |
||||
- The isolation of secrets is to preserve performance when decrypting user data. |
||||
- deserialize(pojo) |
||||
|
||||
### KeyChain (ReduxStore?) |
||||
// attributes |
||||
@name |
||||
|
||||
signTx(txParams, cb) |
||||
signMsg(msg, cb) |
||||
|
||||
getAddressList(cb) |
||||
|
||||
getViewState(cb) -> returns KeyChainViewState |
||||
|
||||
serialize(cb) -> obj |
||||
deserialize(obj) |
||||
|
||||
dispatch({ type: <str>, value: <pojo> }) |
||||
|
||||
|
||||
### KeyChainViewState |
||||
// The serialized, renderable keychain data |
||||
accountList: [], |
||||
typeName: 'uPort', |
||||
iconAddress: 'uport.gif', |
||||
internal: {} // Subclass-defined metadata |
||||
|
||||
### KeyChainReactComponent |
||||
// takes a KeyChainViewState |
||||
|
||||
// Subclasses of this: |
||||
- KeyChainListItemComponent |
||||
- KeyChainInitComponent - Maybe part of the List Item |
||||
- KeyChainAccountHeaderComponent |
||||
- KeyChainConfirmationComponent |
||||
// Account list item, tx confirmation extra data (like a QR code), |
||||
// Maybe an options screen, init screen, |
||||
|
||||
how to send actions? |
||||
emitAction(keychains.<id>.didInit) |
||||
|
||||
|
||||
gimmeRemoteKeychain((err, remoteKeychain)=> |
||||
|
||||
) |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
KeyChainReactComponent({ |
||||
keychain |
||||
}) |
||||
|
||||
Keychain: |
||||
methods:{}, |
||||
cachedAccountList: [], |
||||
name: '', |
||||
|
||||
|
||||
CoinbaseKeychain |
||||
getAccountList |
||||
|
||||
|
||||
CoinbaseKeychainComponent |
||||
isLoading = true |
||||
keychain.getAccountList(()=>{ |
||||
isLoading=false |
||||
accountList=accounts |
||||
}) |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
KeyChainViewState { |
||||
attributes: { |
||||
//mandatory: |
||||
accountList: [], |
||||
typeName: 'uPort', |
||||
iconAddress: 'uport.gif', |
||||
|
||||
internal: { |
||||
// keychain-specific metadata |
||||
proxyAddresses: { |
||||
0xReal: '0xProxy' |
||||
} |
||||
}, |
||||
}, |
||||
methods: { |
||||
// arbitrary, internal |
||||
} |
||||
} |
||||
|
||||
## A note on the security of arbitrary action dispatchers |
||||
|
||||
Since keychains will be dispatching actions that are then passed through the background process to be routed, we should not trust or require them to include their own keychain ID as a prefix to their action, but we should tack it on ourselves, so that no action dispatched by a KeyChainComponent ever reaches any KeyChain other than its own. |
||||
|
@ -1,7 +1,7 @@ |
||||
function wait() { |
||||
function wait(time) { |
||||
return new Promise(function(resolve, reject) { |
||||
setTimeout(function() { |
||||
resolve() |
||||
}, 500) |
||||
}, time * 3 || 1500) |
||||
}) |
||||
} |
||||
|
@ -0,0 +1,21 @@ |
||||
var fs = require('fs') |
||||
var path = require('path') |
||||
var browserify = require('browserify'); |
||||
var tests = fs.readdirSync(path.join(__dirname, 'lib')) |
||||
var bundlePath = path.join(__dirname, 'bundle.js') |
||||
|
||||
var b = browserify(); |
||||
|
||||
// Remove old bundle
|
||||
try { |
||||
fs.unlinkSync(bundlePath) |
||||
} catch (e) {} |
||||
|
||||
var writeStream = fs.createWriteStream(bundlePath) |
||||
|
||||
tests.forEach(function(fileName) { |
||||
b.add(path.join(__dirname, 'lib', fileName)) |
||||
}) |
||||
|
||||
b.bundle().pipe(writeStream); |
||||
|
@ -0,0 +1,90 @@ |
||||
const PASSWORD = 'password123' |
||||
|
||||
QUnit.module('first time usage') |
||||
|
||||
QUnit.test('agree to terms', function (assert) { |
||||
var done = assert.async() |
||||
let app |
||||
|
||||
wait().then(function() { |
||||
app = $('iframe').contents().find('#app-content .mock-app-root') |
||||
app.find('.markdown').prop('scrollTop', 100000000) |
||||
return wait() |
||||
|
||||
}).then(function() { |
||||
|
||||
var title = app.find('h1').text() |
||||
assert.equal(title, 'MetaMask', 'title screen') |
||||
|
||||
var pwBox = app.find('#password-box')[0] |
||||
var confBox = app.find('#password-box-confirm')[0] |
||||
|
||||
pwBox.value = PASSWORD |
||||
confBox.value = PASSWORD |
||||
return wait() |
||||
|
||||
}).then(function() { |
||||
|
||||
var createButton = app.find('button.primary')[0] |
||||
createButton.click() |
||||
|
||||
return wait(1500) |
||||
}).then(function() { |
||||
|
||||
var terms = app.find('h3.terms-header')[0] |
||||
assert.equal(terms.textContent, 'MetaMask Terms & Conditions', 'Showing TOS') |
||||
|
||||
// Scroll through terms
|
||||
var scrollable = app.find('.markdown')[0] |
||||
scrollable.scrollTop = scrollable.scrollHeight |
||||
|
||||
return wait(10) |
||||
}).then(function() { |
||||
|
||||
var button = app.find('button')[0] // Agree button
|
||||
button.click() |
||||
|
||||
return wait(1000) |
||||
}).then(function() { |
||||
|
||||
var created = app.find('h3')[0] |
||||
assert.equal(created.textContent, 'Vault Created', 'Vault created screen') |
||||
|
||||
var button = app.find('button')[0] // Agree button
|
||||
button.click() |
||||
|
||||
return wait(1000) |
||||
}).then(function() { |
||||
|
||||
var detail = app.find('.account-detail-section')[0] |
||||
assert.ok(detail, 'Account detail section loaded.') |
||||
|
||||
var sandwich = app.find('.sandwich-expando')[0] |
||||
sandwich.click() |
||||
|
||||
return wait() |
||||
}).then(function() { |
||||
|
||||
var sandwich = app.find('.menu-droppo')[0] |
||||
var lock = sandwich.children[2] |
||||
assert.ok(lock, 'Lock menu item found') |
||||
lock.click() |
||||
|
||||
return wait(1000) |
||||
}).then(function() { |
||||
|
||||
var pwBox = app.find('#password-box')[0] |
||||
pwBox.value = PASSWORD |
||||
|
||||
var createButton = app.find('button.primary')[0] |
||||
createButton.click() |
||||
|
||||
return wait(1000) |
||||
}).then(function() { |
||||
|
||||
var detail = app.find('.account-detail-section')[0] |
||||
assert.ok(detail, 'Account detail section loaded again.') |
||||
|
||||
done() |
||||
}) |
||||
}) |
@ -0,0 +1,91 @@ |
||||
var ConfigManager = require('../../../app/scripts/lib/config-manager') |
||||
var IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator') |
||||
var SimpleKeyring = require('../../../app/scripts/keyrings/simple') |
||||
var normalize = require('../../../app/scripts/lib/sig-util').normalize |
||||
|
||||
var oldStyleVault = require('../mocks/oldVault.json') |
||||
var badStyleVault = require('../mocks/badVault.json') |
||||
|
||||
var STORAGE_KEY = 'metamask-config' |
||||
var PASSWORD = '12345678' |
||||
var FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase() |
||||
var SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier all dinner' |
||||
|
||||
var BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9' |
||||
|
||||
QUnit.module('Old Style Vaults', { |
||||
beforeEach: function () { |
||||
window.localStorage[STORAGE_KEY] = JSON.stringify(oldStyleVault) |
||||
|
||||
this.configManager = new ConfigManager({ |
||||
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) }, |
||||
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) }, |
||||
}) |
||||
|
||||
this.migrator = new IdStoreMigrator({ |
||||
configManager: this.configManager, |
||||
}) |
||||
} |
||||
}) |
||||
|
||||
QUnit.test('migrator:isInitialized', function (assert) { |
||||
assert.ok(this.migrator) |
||||
}) |
||||
|
||||
QUnit.test('migrator:migratedVaultForPassword', function (assert) { |
||||
var done = assert.async() |
||||
|
||||
this.migrator.migratedVaultForPassword(PASSWORD) |
||||
.then((result) => { |
||||
const { serialized, lostAccounts } = result |
||||
assert.equal(serialized.data.mnemonic, SEED, 'seed phrase recovered') |
||||
assert.equal(lostAccounts.length, 0, 'no lost accounts') |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
QUnit.module('Old Style Vaults with bad HD seed', { |
||||
beforeEach: function () { |
||||
window.localStorage[STORAGE_KEY] = JSON.stringify(badStyleVault) |
||||
|
||||
this.configManager = new ConfigManager({ |
||||
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) }, |
||||
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) }, |
||||
}) |
||||
|
||||
this.migrator = new IdStoreMigrator({ |
||||
configManager: this.configManager, |
||||
}) |
||||
} |
||||
}) |
||||
|
||||
QUnit.test('migrator:migratedVaultForPassword', function (assert) { |
||||
var done = assert.async() |
||||
|
||||
this.migrator.migratedVaultForPassword(PASSWORD) |
||||
.then((result) => { |
||||
const { serialized, lostAccounts } = result |
||||
|
||||
assert.equal(lostAccounts.length, 1, 'one lost account') |
||||
assert.equal(lostAccounts[0].address, '0xe15D894BeCB0354c501AE69429B05143679F39e0'.toLowerCase()) |
||||
assert.ok(lostAccounts[0].privateKey, 'private key exported') |
||||
|
||||
var lostAccount = lostAccounts[0] |
||||
var privateKey = lostAccount.privateKey |
||||
|
||||
var simple = new SimpleKeyring() |
||||
simple.deserialize([privateKey]) |
||||
.then(() => { |
||||
return simple.getAccounts() |
||||
}) |
||||
.then((accounts) => { |
||||
assert.equal(normalize(accounts[0]), lostAccount.address, 'recovered address.') |
||||
done() |
||||
}) |
||||
.catch((reason) => { |
||||
assert.ifError(reason) |
||||
done(reason) |
||||
}) |
||||
}) |
||||
}) |
||||
|
@ -0,0 +1 @@ |
||||
{"meta":{"version":4},"data":{"fiatCurrency":"USD","conversionRate":8.34908448,"conversionDate":1481227505,"isConfirmed":true,"wallet":"{\"encSeed\":{\"encStr\":\"Te2KyAGY3S01bgUJ+7d4y3BOvr/8TKrXrkRZ29cGI6dgyedtN+YgTQxElC2td/pzuoXm7KeSfr+yAoFCvMgqFAJwRcX3arHOsMFQie8kp8mL5I65zwdg/HB2QecB4OJHytrxgApv2zZiKEo0kbu2cs8zYIn5wNlCBIHwgylYmHpUDIJcO1B4zg==\",\"nonce\":\"xnxqk4iy70bjt721F+KPLV4PNfBFNyct\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"vNrSjekRKLmaGFf77Uca9+aAebmDlvrBwtAV8YthpQ4OX/mXtLSycmnLsYdk4schaByfJvrm6/Mf9fxzOSaScJk+XvKw5XqNXedkDHtbWrmNnxFpuT+9tuB8Nupr3D9GZK9PgXhJD99/7Bn6Wk7/ne+PIDmbtdmx/SWmrdo3pg==\",\"nonce\":\"zqWq/gtJ5zfUVRWQQJkP/zoYjer6Rozj\"},\"hdIndex\":1,\"encPrivKeys\":{\"e15d894becb0354c501ae69429b05143679f39e0\":{\"key\":\"jBLQ9v1l5LOEY1C3kI8z7LpbJKHP1vpVfPAlz90MNSfa8Oe+XlxKQAGYs8Zb4fWm\",\"nonce\":\"fJyrSRo1t0RMNqp2MsneoJnYJWHQnSVY\"}},\"addresses\":[\"e15d894becb0354c501ae69429b05143679f39e0\"]}},\"encHdRootPriv\":{\"encStr\":\"mbvwiFBQGbjj4BJLmdeYzfYi8jb7gtFtwiCQOPfvmyz4h2/KMbHNGzumM16qRKpifioQXkhnBulMIQHaYg0Jwv1MoFsqHxHmuIAT+QP5XvJjz0MRl6708pHowmIVG+R8CZNTLqzE7XS8YkZ4ElRpTvLEM8Wngi5Sg287mQMP9w==\",\"nonce\":\"i5Tp2lQe92rXQzNhjZcu9fNNhfux6Wf4\"},\"salt\":\"FQpA8D9R/5qSp9WtQ94FILyfWZHMI6YZw6RmBYqK0N0=\",\"version\":2}","config":{"provider":{"type":"testnet"},"selectedAccount":"0xe15d894becb0354c501ae69429b05143679f39e0"},"isEthConfirmed":true,"transactions":[],"TOSHash":"a4f4e23f823a7ac51783e7ffba7914a911b09acdb97263296b7e14b527f80c5b","gasMultiplier":1}} |
@ -0,0 +1 @@ |
||||
{"meta":{"version":4},"data":{"fiatCurrency":"USD","isConfirmed":true,"TOSHash":"a4f4e23f823a7ac51783e7ffba7914a911b09acdb97263296b7e14b527f80c5b","noticesList":[{"read":true,"date":"Fri Dec 16 2016","title":"Ending Morden Support","body":"Due to [recent events](https://blog.ethereum.org/2016/11/20/from-morden-to-ropsten/), MetaMask is now deprecating support for the Morden Test Network.\n\nUsers will still be able to access Morden through a locally hosted node, but we will no longer be providing hosted access to this network through [Infura](http://infura.io/).\n\nPlease use the new Ropsten Network as your new default test network.\n\nYou can fund your Ropsten account using the buy button on your account page.\n\nBest wishes!\nThe MetaMask Team\n\n","id":0}],"conversionRate":7.07341909,"conversionDate":1482539284,"wallet":"{\"encSeed\":{\"encStr\":\"LZsdN8lJzYkUe1UpmAalnERdgkBFt25gWDdK8kfQUwMAk/27XR+dc+8n5swgoF5qgwhc9LBgliEGNDs1Q/lnuld3aQLabkOeAW4BHS1vS7FxqKrzDS3iyzSuQO6wDQmGno/buuknVgDsKiyjW22hpt7vtVVWA+ZL1P3x6M0+AxGJjeGVrG+E8Q==\",\"nonce\":\"T6O9BmwmTj214XUK3KF0s3iCKo3OlrUD\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"GNNfZevCMlgMVh9y21y1UwrC9qcmH6XYq7v+9UoqbHnzPQJFlxidN5+x/Sldo72a6+5zJpQkkdZ+Q0lePrzvXfuSd3D/RO7WKFIKo9nAQI5+JWwz4INuCmVcmqCv2J4BTLGjrG8fp5pDJ62Bn0XHqkJo3gx3fpvs3cS66+ZKwg==\",\"nonce\":\"HRTlGj44khQs2veYHEF/GqTI1t0yYvyd\"},\"hdIndex\":3,\"encPrivKeys\":{\"e15d894becb0354c501ae69429b05143679f39e0\":{\"key\":\"ZAeZL9VcRUtiiO4VXOQKBFg787PR5R3iymjUeU5vpDRIqOXbjWN6N4ZNR8YpSXl+\",\"nonce\":\"xLsADagS8uqDYae6cImyhxF7o1kBDbPe\"},\"87658c15aefe7448008a28513a11b6b130ef4cd0\":{\"key\":\"ku0mm5s1agRJNAMYIJO0qeoDe+FqcbqdQI6azXF3GL1OLo6uMlt6I4qS+eeravFi\",\"nonce\":\"xdGfSUPKtkW8ge0SWIbbpahs/NyEMzn5\"},\"aa25854c0379e53c957ac9382e720c577fa31fd5\":{\"key\":\"NjpYC9FbiC95CTx/1kwgOHk5LSN9vl4RULEBbvwfVOjqSH8WixNoP3R6I/QyNIs2\",\"nonce\":\"M/HWpXXA9QvuZxEykkGQPJKKdz33ovQr\"}},\"addresses\":[\"e15d894becb0354c501ae69429b05143679f39e0\",\"87658c15aefe7448008a28513a11b6b130ef4cd0\",\"aa25854c0379e53c957ac9382e720c577fa31fd5\"]}},\"encHdRootPriv\":{\"encStr\":\"f+3prUOzl+95aNAV+ad6lZdsYZz120ZsL67ucjj3tiMXf/CC4X8XB9N2QguhoMy6fW+fATUsTdJe8+CbAAyb79V9HY0Pitzq9Yw/g1g0/Ii2JzsdGBriuMsPdwZSVqz+rvQFw/6Qms1xjW6cqa8S7kM2WA5l8RB1Ck6r5zaqbA==\",\"nonce\":\"oGahxNFekVxH9sg6PUCCHIByvo4WFSqm\"},\"salt\":\"N7xYoEA53yhSweOsEphku1UKkIEuZtX2MwLBhVM6RR8=\",\"version\":2}","config":{"provider":{"type":"testnet"},"selectedAccount":"0xe15d894becb0354c501ae69429b05143679f39e0"},"isDisclaimerConfirmed":true,"walletNicknames":{"0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9":"Account 1","0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4":"Account 2","0x1acfb961c5a8268eac8e09d6241a26cbeff42241":"Account 3"},"lostAccounts":["0xe15d894becb0354c501ae69429b05143679f39e0","0x87658c15aefe7448008a28513a11b6b130ef4cd0","0xaa25854c0379e53c957ac9382e720c577fa31fd5"]}} |
@ -0,0 +1,21 @@ |
||||
{ |
||||
"meta": { |
||||
"version": 4 |
||||
}, |
||||
"data": { |
||||
"fiatCurrency": "USD", |
||||
"isConfirmed": true, |
||||
"TOSHash": "a4f4e23f823a7ac51783e7ffba7914a911b09acdb97263296b7e14b527f80c5b", |
||||
"conversionRate": 9.47316629, |
||||
"conversionDate": 1479510994, |
||||
"wallet": "{\"encSeed\":{\"encStr\":\"a5tjKtDGlHkua+6Ta5s3wMFWPmsBqaPdMKGmqeI2z1kMbNs3V03HBaCptU7NtMra1DjHKbSNsUToxFUrmrvWBmUejamN16+l1CviwqASsv7kKzpot00/dfyyJgtZwwFP5Je+TAB1V231nRbPidOfeE1cDec5V8KTF8epl6qzsbA25pjeW76Dfw==\",\"nonce\":\"RzID6bAhWfGTSR74xdIh3RaT1+1sLk6F\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"6nlYAopRbmGcqerRZO08XwgeYaCJg9XRhh4oiYiVVdQtyNPdxvOI9TcE/mqvBiatMwBwA+TmsqTV6eZZe/VDZKYIGajKulQbScd0xQ71JhYfqqmzSG6EH2Pnzwa+aSAsfARgN1JJSaff2+p6wV6Zg5BUDtl72OGEIEfXhcUGwg==\",\"nonce\":\"Ee1KiDqtx7NvYToQUFvjEhKNinNQcXlK\"},\"hdIndex\":1,\"encPrivKeys\":{\"4dd5d356c5a016a220bcd69e82e5af680a430d00\":{\"key\":\"htGRGAH10lGF4M+fvioznmYVIUSWAzwp/yWSIo85psgZZwmCdJY72oyGanYsrFO8\",\"nonce\":\"PkP8XeZ+ok215rzEorvJu9nYTWzkOVr0\"}},\"addresses\":[\"4dd5d356c5a016a220bcd69e82e5af680a430d00\"]}},\"encHdRootPriv\":{\"encStr\":\"TAZAo71a+4IlAaoA66f0w4ts2f+V7ArTSUHRIrMltfAPXz7GfJBmKXNtHPORUYAjRiKqWK6FZnhKLf7Vcng2LG7VnDQwC4xPxzSRZzSEilnoY3V+zRY0HD7Wb/pndb4FliA/buZQmjohO4vezeX0hl70rJlPJEZTyYoWgxbxFA==\",\"nonce\":\"FlJOaLyBEHMaH5fEnYjdHc6nn18+WkRj\"},\"salt\":\"CmuCcWpbqpKUUv+1aE2ZwvQl7EIQ731uFibSq++vwtY=\",\"version\":2}", |
||||
"config": { |
||||
"provider": { |
||||
"type": "testnet" |
||||
}, |
||||
"selectedAccount": "0x4dd5d356c5a016a220bcd69e82e5af680a430d00" |
||||
}, |
||||
"showSeedWords": false, |
||||
"isEthConfirmed": true |
||||
} |
||||
} |
@ -1,24 +0,0 @@ |
||||
QUnit.test('agree to terms', function (assert) { |
||||
var done = assert.async() |
||||
|
||||
// Select the mock app root
|
||||
var app = $('iframe').contents().find('#app-content .mock-app-root') |
||||
|
||||
app.find('.markdown').prop('scrollTop', 100000000) |
||||
|
||||
wait().then(function() { |
||||
app.find('button').click() |
||||
}).then(function() { |
||||
return wait() |
||||
}).then(function() { |
||||
var title = app.find('h1').text() |
||||
assert.equal(title, 'MetaMask', 'title screen') |
||||
|
||||
var buttons = app.find('button') |
||||
assert.equal(buttons.length, 2, 'two buttons: create and restore') |
||||
|
||||
done() |
||||
}) |
||||
|
||||
// Wait for view to transition:
|
||||
}) |
@ -0,0 +1,32 @@ |
||||
var mockHex = '0xabcdef0123456789' |
||||
var mockKey = new Buffer(32) |
||||
let cacheVal |
||||
|
||||
module.exports = { |
||||
|
||||
encrypt(password, dataObj) { |
||||
cacheVal = dataObj |
||||
return Promise.resolve(mockHex) |
||||
}, |
||||
|
||||
decrypt(password, text) { |
||||
return Promise.resolve(cacheVal || {}) |
||||
}, |
||||
|
||||
encryptWithKey(key, dataObj) { |
||||
return this.encrypt(key, dataObj) |
||||
}, |
||||
|
||||
decryptWithKey(key, text) { |
||||
return this.decrypt(key, text) |
||||
}, |
||||
|
||||
keyFromPassword(password) { |
||||
return Promise.resolve(mockKey) |
||||
}, |
||||
|
||||
generateSalt() { |
||||
return 'WHADDASALT!' |
||||
}, |
||||
|
||||
} |
@ -0,0 +1,38 @@ |
||||
var fakeWallet = { |
||||
privKey: '0x123456788890abcdef', |
||||
address: '0xfedcba0987654321', |
||||
} |
||||
const type = 'Simple Key Pair' |
||||
|
||||
module.exports = class MockSimpleKeychain { |
||||
|
||||
static type() { return type } |
||||
|
||||
constructor(opts) { |
||||
this.type = type |
||||
this.opts = opts || {} |
||||
this.wallets = [] |
||||
} |
||||
|
||||
serialize() { |
||||
return [ fakeWallet.privKey ] |
||||
} |
||||
|
||||
deserialize(data) { |
||||
if (!Array.isArray(data)) { |
||||
throw new Error('Simple keychain deserialize requires a privKey array.') |
||||
} |
||||
this.wallets = [ fakeWallet ] |
||||
} |
||||
|
||||
addAccounts(n = 1) { |
||||
for(var i = 0; i < n; i++) { |
||||
this.wallets.push(fakeWallet) |
||||
} |
||||
} |
||||
|
||||
getAccounts() { |
||||
return this.wallets.map(w => w.address) |
||||
} |
||||
|
||||
} |
@ -1,60 +0,0 @@ |
||||
var jsdom = require('mocha-jsdom') |
||||
var assert = require('assert') |
||||
var freeze = require('deep-freeze-strict') |
||||
var path = require('path') |
||||
var sinon = require('sinon') |
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) |
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) |
||||
|
||||
describe('#recoverFromSeed(password, seed)', function() { |
||||
|
||||
beforeEach(function() { |
||||
// sinon allows stubbing methods that are easily verified
|
||||
this.sinon = sinon.sandbox.create() |
||||
}) |
||||
|
||||
afterEach(function() { |
||||
// sinon requires cleanup otherwise it will overwrite context
|
||||
this.sinon.restore() |
||||
}) |
||||
|
||||
// stub out account manager
|
||||
actions._setAccountManager({ |
||||
recoverFromSeed(pw, seed, cb) { |
||||
cb(null, { |
||||
identities: { |
||||
foo: 'bar' |
||||
} |
||||
}) |
||||
}, |
||||
}) |
||||
|
||||
it('sets metamask.isUnlocked to true', function() { |
||||
var initialState = { |
||||
metamask: { |
||||
isUnlocked: false, |
||||
isInitialized: false, |
||||
} |
||||
} |
||||
freeze(initialState) |
||||
|
||||
const restorePhrase = 'invite heavy among daring outdoor dice jelly coil stable note seat vicious' |
||||
const password = 'foo' |
||||
const dispatchFunc = actions.recoverFromSeed(password, restorePhrase) |
||||
|
||||
var dispatchStub = this.sinon.stub() |
||||
dispatchStub.withArgs({ TYPE: actions.unlockMetamask() }).onCall(0) |
||||
dispatchStub.withArgs({ TYPE: actions.showAccountsPage() }).onCall(1) |
||||
|
||||
var action |
||||
var resultingState = initialState |
||||
dispatchFunc((newAction) => { |
||||
action = newAction |
||||
resultingState = reducers(resultingState, action) |
||||
}) |
||||
|
||||
assert.equal(resultingState.metamask.isUnlocked, true, 'was unlocked') |
||||
assert.equal(resultingState.metamask.isInitialized, true, 'was initialized') |
||||
}); |
||||
}); |
@ -0,0 +1,146 @@ |
||||
const async = require('async') |
||||
const assert = require('assert') |
||||
const ethUtil = require('ethereumjs-util') |
||||
const BN = ethUtil.BN |
||||
const ConfigManager = require('../../app/scripts/lib/config-manager') |
||||
const delegateCallCode = require('../lib/example-code.json').delegateCallCode |
||||
|
||||
// The old way:
|
||||
const IdentityStore = require('../../app/scripts/lib/idStore') |
||||
const STORAGE_KEY = 'metamask-config' |
||||
const extend = require('xtend') |
||||
|
||||
// The new ways:
|
||||
var KeyringController = require('../../app/scripts/keyring-controller') |
||||
const mockEncryptor = require('../lib/mock-encryptor') |
||||
const MockSimpleKeychain = require('../lib/mock-simple-keychain') |
||||
const sinon = require('sinon') |
||||
|
||||
const mockVault = { |
||||
seed: 'picnic injury awful upper eagle junk alert toss flower renew silly vague', |
||||
account: '0x5d8de92c205279c10e5669f797b853ccef4f739a', |
||||
} |
||||
|
||||
const badVault = { |
||||
seed: 'radar blur cabbage chef fix engine embark joy scheme fiction master release', |
||||
} |
||||
|
||||
describe('IdentityStore to KeyringController migration', function() { |
||||
|
||||
// The stars of the show:
|
||||
let idStore, keyringController, seedWords, configManager |
||||
|
||||
let password = 'password123' |
||||
let entropy = 'entripppppyy duuude' |
||||
let accounts = [] |
||||
let newAccounts = [] |
||||
let originalKeystore |
||||
|
||||
// This is a lot of setup, I know!
|
||||
// We have to create an old style vault, populate it,
|
||||
// and THEN create a new one, before we can run tests on it.
|
||||
beforeEach(function(done) { |
||||
this.sinon = sinon.sandbox.create() |
||||
window.localStorage = {} // Hacking localStorage support into JSDom
|
||||
configManager = new ConfigManager({ |
||||
loadData, |
||||
setData: (d) => { window.localStorage = d } |
||||
}) |
||||
|
||||
|
||||
idStore = new IdentityStore({ |
||||
configManager: configManager, |
||||
ethStore: { |
||||
addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, |
||||
del(acct) { delete accounts[acct] }, |
||||
}, |
||||
}) |
||||
|
||||
idStore._createVault(password, mockVault.seed, (err) => { |
||||
assert.ifError(err, 'createNewVault threw error') |
||||
originalKeystore = idStore._idmgmt.keyStore |
||||
|
||||
idStore.setLocked((err) => { |
||||
assert.ifError(err, 'createNewVault threw error') |
||||
keyringController = new KeyringController({ |
||||
configManager, |
||||
ethStore: { |
||||
addAccount(acct) { newAccounts.push(ethUtil.addHexPrefix(acct)) }, |
||||
del(acct) { delete newAccounts[acct] }, |
||||
}, |
||||
txManager: { |
||||
getTxList: () => [], |
||||
getUnapprovedTxList: () => [] |
||||
}, |
||||
}) |
||||
|
||||
// Stub out the browser crypto for a mock encryptor.
|
||||
// Browser crypto is tested in the integration test suite.
|
||||
keyringController.encryptor = mockEncryptor |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('entering a password', function() { |
||||
it('should identify an old wallet as an initialized keyring', function(done) { |
||||
keyringController.configManager.setWallet('something') |
||||
keyringController.getState() |
||||
.then((state) => { |
||||
assert(state.isInitialized, 'old vault counted as initialized.') |
||||
assert(!state.lostAccounts, 'no lost accounts') |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
function loadData () { |
||||
var oldData = getOldStyleData() |
||||
var newData |
||||
try { |
||||
newData = JSON.parse(window.localStorage[STORAGE_KEY]) |
||||
} catch (e) {} |
||||
|
||||
var data = extend({ |
||||
meta: { |
||||
version: 0, |
||||
}, |
||||
data: { |
||||
config: { |
||||
provider: { |
||||
type: 'testnet', |
||||
}, |
||||
}, |
||||
}, |
||||
}, oldData || null, newData || null) |
||||
return data |
||||
} |
||||
|
||||
function setData (data) { |
||||
window.localStorage[STORAGE_KEY] = JSON.stringify(data) |
||||
} |
||||
|
||||
function getOldStyleData () { |
||||
var config, wallet, seedWords |
||||
|
||||
var result = { |
||||
meta: { version: 0 }, |
||||
data: {}, |
||||
} |
||||
|
||||
try { |
||||
config = JSON.parse(window.localStorage['config']) |
||||
result.data.config = config |
||||
} catch (e) {} |
||||
try { |
||||
wallet = JSON.parse(window.localStorage['lightwallet']) |
||||
result.data.wallet = wallet |
||||
} catch (e) {} |
||||
try { |
||||
seedWords = window.localStorage['seedWords'] |
||||
result.data.seedWords = seedWords |
||||
} catch (e) {} |
||||
|
||||
return result |
||||
} |
@ -0,0 +1,189 @@ |
||||
var assert = require('assert') |
||||
var KeyringController = require('../../app/scripts/keyring-controller') |
||||
var configManagerGen = require('../lib/mock-config-manager') |
||||
const ethUtil = require('ethereumjs-util') |
||||
const BN = ethUtil.BN |
||||
const async = require('async') |
||||
const mockEncryptor = require('../lib/mock-encryptor') |
||||
const MockSimpleKeychain = require('../lib/mock-simple-keychain') |
||||
const sinon = require('sinon') |
||||
|
||||
describe('KeyringController', function() { |
||||
|
||||
let keyringController, state |
||||
let password = 'password123' |
||||
let seedWords = 'puzzle seed penalty soldier say clay field arctic metal hen cage runway' |
||||
let addresses = ['eF35cA8EbB9669A35c31b5F6f249A9941a812AC1'.toLowerCase()] |
||||
let accounts = [] |
||||
let originalKeystore |
||||
|
||||
beforeEach(function(done) { |
||||
this.sinon = sinon.sandbox.create() |
||||
window.localStorage = {} // Hacking localStorage support into JSDom
|
||||
|
||||
keyringController = new KeyringController({ |
||||
configManager: configManagerGen(), |
||||
txManager: { |
||||
getTxList: () => [], |
||||
getUnapprovedTxList: () => [] |
||||
}, |
||||
ethStore: { |
||||
addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, |
||||
}, |
||||
}) |
||||
|
||||
// Stub out the browser crypto for a mock encryptor.
|
||||
// Browser crypto is tested in the integration test suite.
|
||||
keyringController.encryptor = mockEncryptor |
||||
|
||||
keyringController.createNewVaultAndKeychain(password) |
||||
.then(function (newState) { |
||||
state = newState |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
afterEach(function() { |
||||
// Cleanup mocks
|
||||
this.sinon.restore() |
||||
}) |
||||
|
||||
describe('#createNewVaultAndKeychain', function () { |
||||
this.timeout(10000) |
||||
|
||||
it('should set a vault on the configManager', function(done) { |
||||
keyringController.configManager.setVault(null) |
||||
assert(!keyringController.configManager.getVault(), 'no previous vault') |
||||
keyringController.createNewVaultAndKeychain(password) |
||||
.then(() => { |
||||
const vault = keyringController.configManager.getVault() |
||||
assert(vault, 'vault created') |
||||
done() |
||||
}) |
||||
.catch((reason) => { |
||||
assert.ifError(reason) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#restoreKeyring', function() { |
||||
|
||||
it(`should pass a keyring's serialized data back to the correct type.`, function(done) { |
||||
const mockSerialized = { |
||||
type: 'HD Key Tree', |
||||
data: { |
||||
mnemonic: seedWords, |
||||
numberOfAccounts: 1, |
||||
} |
||||
} |
||||
const mock = this.sinon.mock(keyringController) |
||||
|
||||
mock.expects('getBalanceAndNickname') |
||||
.exactly(1) |
||||
|
||||
keyringController.restoreKeyring(mockSerialized) |
||||
.then((keyring) => { |
||||
assert.equal(keyring.wallets.length, 1, 'one wallet restored') |
||||
return keyring.getAccounts() |
||||
}) |
||||
.then((accounts) => { |
||||
assert.equal(accounts[0], addresses[0]) |
||||
mock.verify() |
||||
done() |
||||
}) |
||||
.catch((reason) => { |
||||
assert.ifError(reason) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#createNickname', function() { |
||||
it('should add the address to the identities hash', function() { |
||||
const fakeAddress = '0x12345678' |
||||
keyringController.createNickname(fakeAddress) |
||||
const identities = keyringController.identities |
||||
const identity = identities[fakeAddress] |
||||
assert.equal(identity.address, fakeAddress) |
||||
|
||||
const nick = keyringController.configManager.nicknameForWallet(fakeAddress) |
||||
assert.equal(typeof nick, 'string') |
||||
}) |
||||
}) |
||||
|
||||
describe('#saveAccountLabel', function() { |
||||
it ('sets the nickname', function(done) { |
||||
const account = addresses[0] |
||||
var nick = 'Test nickname' |
||||
keyringController.identities[ethUtil.addHexPrefix(account)] = {} |
||||
keyringController.saveAccountLabel(account, nick) |
||||
.then((label) => { |
||||
assert.equal(label, nick) |
||||
const persisted = keyringController.configManager.nicknameForWallet(account) |
||||
assert.equal(persisted, nick) |
||||
done() |
||||
}) |
||||
.catch((reason) => { |
||||
assert.ifError(reason) |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
this.timeout(10000) |
||||
it('retrieves the persisted nickname', function(done) { |
||||
const account = addresses[0] |
||||
var nick = 'Test nickname' |
||||
keyringController.configManager.setNicknameForWallet(account, nick) |
||||
keyringController.createNewVaultAndRestore(password, seedWords) |
||||
.then((state) => { |
||||
|
||||
const identity = keyringController.identities['0x' + account] |
||||
assert.equal(identity.name, nick) |
||||
|
||||
assert(accounts) |
||||
done() |
||||
}) |
||||
.catch((reason) => { |
||||
assert.ifError(reason) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#getAccounts', function() { |
||||
it('returns the result of getAccounts for each keyring', function() { |
||||
keyringController.keyrings = [ |
||||
{ getAccounts() { return Promise.resolve([1,2,3]) } }, |
||||
{ getAccounts() { return Promise.resolve([4,5,6]) } }, |
||||
] |
||||
|
||||
keyringController.getAccounts() |
||||
.then((result) => { |
||||
assert.deepEqual(result, [1,2,3,4,5,6]) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#addGasBuffer', function() { |
||||
it('adds 100k gas buffer to estimates', function() { |
||||
|
||||
const gas = '0x04ee59' // Actual estimated gas example
|
||||
const tooBigOutput = '0x80674f9' // Actual bad output
|
||||
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) |
||||
const correctBuffer = new BN('100000', 10) |
||||
const correct = bnGas.add(correctBuffer) |
||||
|
||||
const tooBig = new BN(tooBigOutput, 16) |
||||
const result = keyringController.addGasBuffer(gas) |
||||
const bnResult = new BN(ethUtil.stripHexPrefix(result), 16) |
||||
|
||||
assert.equal(result.indexOf('0x'), 0, 'included hex prefix') |
||||
assert(bnResult.gt(bnGas), 'Estimate increased in value.') |
||||
assert.equal(bnResult.sub(bnGas).toString(10), '100000', 'added 100k gas') |
||||
assert.equal(result, '0x' + correct.toString(16), 'Added the right amount') |
||||
assert.notEqual(result, tooBigOutput, 'not that bad estimate') |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,127 @@ |
||||
const assert = require('assert') |
||||
const extend = require('xtend') |
||||
const HdKeyring = require('../../../app/scripts/keyrings/hd') |
||||
|
||||
// Sample account:
|
||||
const privKeyHex = 'b8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952' |
||||
|
||||
const sampleMnemonic = 'finish oppose decorate face calm tragic certain desk hour urge dinosaur mango' |
||||
const firstAcct = '1c96099350f13d558464ec79b9be4445aa0ef579' |
||||
const secondAcct = '1b00aed43a693f3a957f9feb5cc08afa031e37a0' |
||||
|
||||
describe('hd-keyring', function() { |
||||
|
||||
let keyring |
||||
beforeEach(function() { |
||||
keyring = new HdKeyring() |
||||
}) |
||||
|
||||
describe('constructor', function(done) { |
||||
keyring = new HdKeyring({ |
||||
mnemonic: sampleMnemonic, |
||||
numberOfAccounts: 2, |
||||
}) |
||||
|
||||
const accounts = keyring.getAccounts() |
||||
.then((accounts) => { |
||||
assert.equal(accounts[0], firstAcct) |
||||
assert.equal(accounts[1], secondAcct) |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
describe('Keyring.type', function() { |
||||
it('is a class property that returns the type string.', function() { |
||||
const type = HdKeyring.type |
||||
assert.equal(typeof type, 'string') |
||||
}) |
||||
}) |
||||
|
||||
describe('#type', function() { |
||||
it('returns the correct value', function() { |
||||
const type = keyring.type |
||||
const correct = HdKeyring.type |
||||
assert.equal(type, correct) |
||||
}) |
||||
}) |
||||
|
||||
describe('#serialize empty wallets.', function() { |
||||
it('serializes a new mnemonic', function() { |
||||
keyring.serialize() |
||||
.then((output) => { |
||||
assert.equal(output.numberOfAccounts, 0) |
||||
assert.equal(output.mnemonic, null) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#deserialize a private key', function() { |
||||
it('serializes what it deserializes', function(done) { |
||||
keyring.deserialize({ |
||||
mnemonic: sampleMnemonic, |
||||
numberOfAccounts: 1 |
||||
}) |
||||
.then(() => { |
||||
assert.equal(keyring.wallets.length, 1, 'restores two accounts') |
||||
return keyring.addAccounts(1) |
||||
}).then(() => { |
||||
return keyring.getAccounts() |
||||
}).then((accounts) => { |
||||
assert.equal(accounts[0], firstAcct) |
||||
assert.equal(accounts[1], secondAcct) |
||||
assert.equal(accounts.length, 2) |
||||
|
||||
return keyring.serialize() |
||||
}).then((serialized) => { |
||||
assert.equal(serialized.mnemonic, sampleMnemonic) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#addAccounts', function() { |
||||
describe('with no arguments', function() { |
||||
it('creates a single wallet', function(done) { |
||||
keyring.addAccounts() |
||||
.then(() => { |
||||
assert.equal(keyring.wallets.length, 1) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('with a numeric argument', function() { |
||||
it('creates that number of wallets', function(done) { |
||||
keyring.addAccounts(3) |
||||
.then(() => { |
||||
assert.equal(keyring.wallets.length, 3) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#getAccounts', function() { |
||||
it('calls getAddress on each wallet', function(done) { |
||||
|
||||
// Push a mock wallet
|
||||
const desiredOutput = 'foo' |
||||
keyring.wallets.push({ |
||||
getAddress() { |
||||
return { |
||||
toString() { |
||||
return desiredOutput |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
|
||||
const output = keyring.getAccounts() |
||||
.then((output) => { |
||||
assert.equal(output[0], desiredOutput) |
||||
assert.equal(output.length, 1) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,91 @@ |
||||
const assert = require('assert') |
||||
const extend = require('xtend') |
||||
const ethUtil = require('ethereumjs-util') |
||||
const SimpleKeyring = require('../../../app/scripts/keyrings/simple') |
||||
const TYPE_STR = 'Simple Key Pair' |
||||
|
||||
// Sample account:
|
||||
const privKeyHex = 'b8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952' |
||||
|
||||
describe('simple-keyring', function() { |
||||
|
||||
let keyring |
||||
beforeEach(function() { |
||||
keyring = new SimpleKeyring() |
||||
}) |
||||
|
||||
describe('Keyring.type', function() { |
||||
it('is a class property that returns the type string.', function() { |
||||
const type = SimpleKeyring.type |
||||
assert.equal(type, TYPE_STR) |
||||
}) |
||||
}) |
||||
|
||||
describe('#type', function() { |
||||
it('returns the correct value', function() { |
||||
const type = keyring.type |
||||
assert.equal(type, TYPE_STR) |
||||
}) |
||||
}) |
||||
|
||||
describe('#serialize empty wallets.', function() { |
||||
it('serializes an empty array', function(done) { |
||||
keyring.serialize() |
||||
.then((output) => { |
||||
assert.deepEqual(output, []) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#deserialize a private key', function() { |
||||
it('serializes what it deserializes', function() { |
||||
keyring.deserialize([privKeyHex]) |
||||
.then(() => { |
||||
assert.equal(keyring.wallets.length, 1, 'has one wallet') |
||||
const serialized = keyring.serialize() |
||||
assert.equal(serialized[0], privKeyHex) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#addAccounts', function() { |
||||
describe('with no arguments', function() { |
||||
it('creates a single wallet', function() { |
||||
keyring.addAccounts() |
||||
.then(() => { |
||||
assert.equal(keyring.wallets.length, 1) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('with a numeric argument', function() { |
||||
it('creates that number of wallets', function() { |
||||
keyring.addAccounts(3) |
||||
.then(() => { |
||||
assert.equal(keyring.wallets.length, 3) |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#getAccounts', function() { |
||||
it('calls getAddress on each wallet', function(done) { |
||||
|
||||
// Push a mock wallet
|
||||
const desiredOutput = '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761' |
||||
keyring.wallets.push({ |
||||
getAddress() { |
||||
return ethUtil.toBuffer(desiredOutput) |
||||
} |
||||
}) |
||||
|
||||
keyring.getAccounts() |
||||
.then((output) => { |
||||
assert.equal(output[0], desiredOutput) |
||||
assert.equal(output.length, 1) |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,22 @@ |
||||
const assert = require('assert') |
||||
const nodeify = require('../../app/scripts/lib/nodeify') |
||||
|
||||
describe('nodeify', function() { |
||||
|
||||
var obj = { |
||||
foo: 'bar', |
||||
promiseFunc: function (a) { |
||||
var solution = this.foo + a |
||||
return Promise.resolve(solution) |
||||
} |
||||
} |
||||
|
||||
it('should retain original context', function(done) { |
||||
var nodified = nodeify(obj.promiseFunc).bind(obj) |
||||
nodified('baz', function (err, res) { |
||||
assert.equal(res, 'barbaz') |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,215 @@ |
||||
const assert = require('assert') |
||||
const extend = require('xtend') |
||||
const EventEmitter = require('events') |
||||
const STORAGE_KEY = 'metamask-persistance-key' |
||||
const TransactionManager = require('../../app/scripts/transaction-manager') |
||||
|
||||
describe('Transaction Manager', function() { |
||||
let txManager |
||||
|
||||
const onTxDoneCb = () => true |
||||
beforeEach(function() { |
||||
txManager = new TransactionManager ({ |
||||
txList: [], |
||||
setTxList: () => {}, |
||||
provider: "testnet", |
||||
txHistoryLimit: 10, |
||||
blockTracker: new EventEmitter(), |
||||
getNetwork: function(){ return 'unit test' } |
||||
}) |
||||
}) |
||||
|
||||
describe('#validateTxParams', function () { |
||||
it('returns null for positive values', function() { |
||||
var sample = { |
||||
value: '0x01' |
||||
} |
||||
var res = txManager.txProviderUtils.validateTxParams(sample, (err) => { |
||||
assert.equal(err, null, 'no error') |
||||
}) |
||||
}) |
||||
|
||||
|
||||
it('returns error for negative values', function() { |
||||
var sample = { |
||||
value: '-0x01' |
||||
} |
||||
var res = txManager.txProviderUtils.validateTxParams(sample, (err) => { |
||||
assert.ok(err, 'error') |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#getTxList', function() { |
||||
it('when new should return empty array', function() { |
||||
var result = txManager.getTxList() |
||||
assert.ok(Array.isArray(result)) |
||||
assert.equal(result.length, 0) |
||||
}) |
||||
it('should also return transactions from local storage if any', function() { |
||||
|
||||
}) |
||||
}) |
||||
|
||||
describe('#_saveTxList', function() { |
||||
it('saves the submitted data to the tx list', function() { |
||||
var target = [{ foo: 'bar', metamaskNetworkId: 'unit test' }] |
||||
txManager._saveTxList(target) |
||||
var result = txManager.getTxList() |
||||
assert.equal(result[0].foo, 'bar') |
||||
}) |
||||
}) |
||||
|
||||
describe('#addTx', function() { |
||||
it('adds a tx returned in getTxList', function() { |
||||
var tx = { id: 1, status: 'confirmed', metamaskNetworkId: 'unit test' } |
||||
txManager.addTx(tx, onTxDoneCb) |
||||
var result = txManager.getTxList() |
||||
assert.ok(Array.isArray(result)) |
||||
assert.equal(result.length, 1) |
||||
assert.equal(result[0].id, 1) |
||||
}) |
||||
|
||||
it('cuts off early txs beyond a limit', function() { |
||||
const limit = txManager.txHistoryLimit |
||||
for (let i = 0; i < limit + 1; i++) { |
||||
let tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: 'unit test' } |
||||
txManager.addTx(tx, onTxDoneCb) |
||||
} |
||||
var result = txManager.getTxList() |
||||
assert.equal(result.length, limit, `limit of ${limit} txs enforced`) |
||||
assert.equal(result[0].id, 1, 'early txs truncted') |
||||
}) |
||||
|
||||
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function() { |
||||
const limit = txManager.txHistoryLimit |
||||
for (let i = 0; i < limit + 1; i++) { |
||||
let tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: 'unit test' } |
||||
txManager.addTx(tx, onTxDoneCb) |
||||
} |
||||
var result = txManager.getTxList() |
||||
assert.equal(result.length, limit, `limit of ${limit} txs enforced`) |
||||
assert.equal(result[0].id, 1, 'early txs truncted') |
||||
}) |
||||
|
||||
it('cuts off early txs beyond a limit but does not cut unapproved txs', function() { |
||||
var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: 'unit test' } |
||||
txManager.addTx(unconfirmedTx, onTxDoneCb) |
||||
const limit = txManager.txHistoryLimit |
||||
for (let i = 1; i < limit + 1; i++) { |
||||
let tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: 'unit test' } |
||||
txManager.addTx(tx, onTxDoneCb) |
||||
} |
||||
var result = txManager.getTxList() |
||||
assert.equal(result.length, limit, `limit of ${limit} txs enforced`) |
||||
assert.equal(result[0].id, 0, 'first tx should still be there') |
||||
assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved') |
||||
assert.equal(result[1].id, 2, 'early txs truncted') |
||||
}) |
||||
}) |
||||
|
||||
describe('#setTxStatusSigned', function() { |
||||
it('sets the tx status to signed', function() { |
||||
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } |
||||
txManager.addTx(tx, onTxDoneCb) |
||||
txManager.setTxStatusSigned(1) |
||||
var result = txManager.getTxList() |
||||
assert.ok(Array.isArray(result)) |
||||
assert.equal(result.length, 1) |
||||
assert.equal(result[0].status, 'signed') |
||||
}) |
||||
|
||||
it('should emit a signed event to signal the exciton of callback', (done) => { |
||||
this.timeout(10000) |
||||
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } |
||||
let onTxDoneCb = function () { |
||||
assert(true, 'event listener has been triggered and onTxDoneCb executed') |
||||
done() |
||||
} |
||||
txManager.addTx(tx) |
||||
txManager.on('1:signed', onTxDoneCb) |
||||
txManager.setTxStatusSigned(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('#setTxStatusRejected', function() { |
||||
it('sets the tx status to rejected', function() { |
||||
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } |
||||
txManager.addTx(tx) |
||||
txManager.setTxStatusRejected(1) |
||||
var result = txManager.getTxList() |
||||
assert.ok(Array.isArray(result)) |
||||
assert.equal(result.length, 1) |
||||
assert.equal(result[0].status, 'rejected') |
||||
}) |
||||
|
||||
it('should emit a rejected event to signal the exciton of callback', (done) => { |
||||
this.timeout(10000) |
||||
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } |
||||
txManager.addTx(tx) |
||||
let onTxDoneCb = function (err, txId) { |
||||
assert(true, 'event listener has been triggered and onTxDoneCb executed') |
||||
done() |
||||
} |
||||
txManager.on('1:rejected', onTxDoneCb) |
||||
txManager.setTxStatusRejected(1) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#updateTx', function() { |
||||
it('replaces the tx with the same id', function() { |
||||
txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }, onTxDoneCb) |
||||
txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: 'unit test' }, onTxDoneCb) |
||||
txManager.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: 'unit test' }) |
||||
var result = txManager.getTx('1') |
||||
assert.equal(result.hash, 'foo') |
||||
}) |
||||
}) |
||||
|
||||
describe('#getUnapprovedTxList', function() { |
||||
it('returns unapproved txs in a hash', function() { |
||||
txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }, onTxDoneCb) |
||||
txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: 'unit test' }, onTxDoneCb) |
||||
let result = txManager.getUnapprovedTxList() |
||||
assert.equal(typeof result, 'object') |
||||
assert.equal(result['1'].status, 'unapproved') |
||||
assert.equal(result['2'], undefined) |
||||
}) |
||||
}) |
||||
|
||||
describe('#getTx', function() { |
||||
it('returns a tx with the requested id', function() { |
||||
txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }, onTxDoneCb) |
||||
txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: 'unit test' }, onTxDoneCb) |
||||
assert.equal(txManager.getTx('1').status, 'unapproved') |
||||
assert.equal(txManager.getTx('2').status, 'confirmed') |
||||
}) |
||||
}) |
||||
|
||||
describe('#getFilteredTxList', function() { |
||||
it('returns a tx with the requested data', function() { |
||||
var foop = 0 |
||||
var zoop = 0 |
||||
for (let i = 0; i < 10; ++i ){ |
||||
let everyOther = i % 2 |
||||
txManager.addTx({ id: i, |
||||
status: everyOther ? 'unapproved' : 'confirmed', |
||||
metamaskNetworkId: 'unit test', |
||||
txParams: { |
||||
from: everyOther ? 'foop' : 'zoop', |
||||
to: everyOther ? 'zoop' : 'foop', |
||||
} |
||||
}, onTxDoneCb) |
||||
everyOther ? ++foop : ++zoop |
||||
} |
||||
assert.equal(txManager.getFilteredTxList({status: 'confirmed', from: 'zoop'}).length, zoop) |
||||
assert.equal(txManager.getFilteredTxList({status: 'confirmed', to: 'foop'}).length, zoop) |
||||
assert.equal(txManager.getFilteredTxList({status: 'confirmed', from: 'foop'}).length, 0) |
||||
assert.equal(txManager.getFilteredTxList({status: 'confirmed'}).length, zoop) |
||||
assert.equal(txManager.getFilteredTxList({from: 'foop'}).length, foop) |
||||
assert.equal(txManager.getFilteredTxList({from: 'zoop'}).length, zoop) |
||||
}) |
||||
}) |
||||
|
||||
}) |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue