mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +01:00
Merge branch 'dev' into TxManager
This commit is contained in:
commit
6f7c23fd28
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
dist
|
||||
npm-debug.log
|
||||
node_modules
|
||||
temp
|
||||
.tmp
|
||||
@ -13,4 +14,4 @@ app/.DS_Store
|
||||
development/bundle.js
|
||||
builds.zip
|
||||
test/integration/bundle.js
|
||||
npm-debug.log
|
||||
development/states.js
|
||||
|
16
CHANGELOG.md
16
CHANGELOG.md
@ -3,7 +3,23 @@
|
||||
## Current Master
|
||||
|
||||
- Add a check for when a tx is included in a block.
|
||||
|
||||
## 2.14.1 2016-12-20
|
||||
|
||||
- Temporarily disable extension reload detection causing infinite reload bug.
|
||||
- Implemented basic checking for valid RPC URIs.
|
||||
|
||||
## 2.14.0 2016-12-16
|
||||
|
||||
- Removed Morden testnet provider from provider menu.
|
||||
- Add support for notices.
|
||||
- Fix broken reload detection.
|
||||
- Fix transaction forever cached-as-pending bug.
|
||||
|
||||
## 2.13.11 2016-11-23
|
||||
|
||||
- Add support for synchronous RPC method "eth_uninstallFilter".
|
||||
- Forgotten password prompts now send users directly to seed word restoration.
|
||||
|
||||
## 2.13.10 2016-11-22
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "MetaMask",
|
||||
"short_name": "Metamask",
|
||||
"version": "2.13.10",
|
||||
"version": "2.14.1",
|
||||
"manifest_version": 2,
|
||||
"author": "https://metamask.io",
|
||||
"description": "Ethereum Browser Extension",
|
||||
|
@ -1,6 +1,5 @@
|
||||
const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
|
||||
const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask'
|
||||
const MORDEN_RPC_URL = 'https://morden.infura.io/metamask'
|
||||
const DEFAULT_RPC_URL = TESTNET_RPC_URL
|
||||
|
||||
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
||||
@ -11,6 +10,6 @@ module.exports = {
|
||||
default: DEFAULT_RPC_URL,
|
||||
mainnet: MAINET_RPC_URL,
|
||||
testnet: TESTNET_RPC_URL,
|
||||
morden: MORDEN_RPC_URL,
|
||||
morden: TESTNET_RPC_URL,
|
||||
},
|
||||
}
|
||||
|
@ -2,8 +2,8 @@
|
||||
cleanContextForImports()
|
||||
require('web3/dist/web3.min.js')
|
||||
const LocalMessageDuplexStream = require('post-message-stream')
|
||||
const PingStream = require('ping-pong-stream/ping')
|
||||
const endOfStream = require('end-of-stream')
|
||||
// const PingStream = require('ping-pong-stream/ping')
|
||||
// const endOfStream = require('end-of-stream')
|
||||
const setupDappAutoReload = require('./lib/auto-reload.js')
|
||||
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
|
||||
restoreContextAfterImports()
|
||||
@ -40,13 +40,15 @@ reloadStream.once('data', triggerReload)
|
||||
|
||||
// setup ping timeout autoreload
|
||||
// LocalMessageDuplexStream does not self-close, so reload if pingStream fails
|
||||
var pingChannel = inpageProvider.multiStream.createStream('pingpong')
|
||||
var pingStream = new PingStream({ objectMode: true })
|
||||
// var pingChannel = inpageProvider.multiStream.createStream('pingpong')
|
||||
// var pingStream = new PingStream({ objectMode: true })
|
||||
// wait for first successful reponse
|
||||
metamaskStream.once('_data', function () {
|
||||
pingStream.pipe(pingChannel).pipe(pingStream)
|
||||
})
|
||||
endOfStream(pingStream, triggerReload)
|
||||
|
||||
// disable pingStream until https://github.com/MetaMask/metamask-plugin/issues/746 is resolved more gracefully
|
||||
// metamaskStream.once('data', function(){
|
||||
// pingStream.pipe(pingChannel).pipe(pingStream)
|
||||
// })
|
||||
// endOfStream(pingStream, triggerReload)
|
||||
|
||||
// set web3 defaultAcount
|
||||
inpageProvider.publicConfigStore.subscribe(function (state) {
|
||||
|
@ -2,8 +2,9 @@ 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 encryptor = require('./lib/encryptor')
|
||||
const messageManager = require('./lib/message-manager')
|
||||
const IdStoreMigrator = require('./lib/idStore-migrator')
|
||||
const BN = ethUtil.BN
|
||||
@ -106,6 +107,7 @@ module.exports = class KeyringController extends EventEmitter {
|
||||
conversionDate: this.configManager.getConversionDate(),
|
||||
keyringTypes: this.keyringTypes.map(krt => krt.type),
|
||||
identities: this.identities,
|
||||
lostAccounts: this.configManager.getLostAccounts(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -430,11 +432,17 @@ module.exports = class KeyringController extends EventEmitter {
|
||||
// may be completed without interruption.
|
||||
migrateOldVaultIfAny (password) {
|
||||
const shouldMigrate = !!this.configManager.getWallet() && !this.configManager.getVault()
|
||||
if (!shouldMigrate) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return this.idStoreMigrator.migratedVaultForPassword(password)
|
||||
.then((serialized) => {
|
||||
.then((result) => {
|
||||
this.password = password
|
||||
|
||||
if (serialized && shouldMigrate) {
|
||||
if (result && shouldMigrate) {
|
||||
const { serialized, lostAccounts } = result
|
||||
this.configManager.setLostAccounts(lostAccounts)
|
||||
return this.restoreKeyring(serialized)
|
||||
.then(keyring => keyring.getAccounts())
|
||||
.then((accounts) => {
|
||||
|
@ -33,15 +33,15 @@ class HdKeyring extends EventEmitter {
|
||||
this.mnemonic = null
|
||||
this.root = null
|
||||
|
||||
if ('mnemonic' in opts) {
|
||||
if (opts.mnemonic) {
|
||||
this._initFromMnemonic(opts.mnemonic)
|
||||
}
|
||||
|
||||
if ('numberOfAccounts' in opts) {
|
||||
this.addAccounts(opts.numberOfAccounts)
|
||||
if (opts.numberOfAccounts) {
|
||||
return this.addAccounts(opts.numberOfAccounts)
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
addAccounts (numberOfAccounts = 1) {
|
||||
|
@ -380,3 +380,14 @@ ConfigManager.prototype.setGasMultiplier = function (gasMultiplier) {
|
||||
data.gasMultiplier = gasMultiplier
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setLostAccounts = function (lostAccounts) {
|
||||
var data = this.getData()
|
||||
data.lostAccounts = lostAccounts
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getLostAccounts = function () {
|
||||
var data = this.getData()
|
||||
return data.lostAccounts || []
|
||||
}
|
||||
|
@ -1,156 +0,0 @@
|
||||
module.exports = {
|
||||
|
||||
// Simple encryption methods:
|
||||
encrypt,
|
||||
decrypt,
|
||||
|
||||
// More advanced encryption methods:
|
||||
keyFromPassword,
|
||||
encryptWithKey,
|
||||
decryptWithKey,
|
||||
|
||||
// Buffer <-> String methods
|
||||
convertArrayBufferViewtoString,
|
||||
convertStringToArrayBufferView,
|
||||
|
||||
// Buffer <-> Hex string methods
|
||||
serializeBufferForStorage,
|
||||
serializeBufferFromStorage,
|
||||
|
||||
// Buffer <-> base64 string methods
|
||||
encodeBufferToBase64,
|
||||
decodeBase64ToBuffer,
|
||||
|
||||
generateSalt,
|
||||
}
|
||||
|
||||
// Takes a Pojo, returns cypher text.
|
||||
function encrypt (password, dataObj) {
|
||||
const salt = this.generateSalt()
|
||||
|
||||
return keyFromPassword(password + salt)
|
||||
.then(function (passwordDerivedKey) {
|
||||
return encryptWithKey(passwordDerivedKey, dataObj)
|
||||
})
|
||||
.then(function (payload) {
|
||||
payload.salt = salt
|
||||
return JSON.stringify(payload)
|
||||
})
|
||||
}
|
||||
|
||||
function encryptWithKey (key, dataObj) {
|
||||
var data = JSON.stringify(dataObj)
|
||||
var dataBuffer = convertStringToArrayBufferView(data)
|
||||
var vector = global.crypto.getRandomValues(new Uint8Array(16))
|
||||
return global.crypto.subtle.encrypt({
|
||||
name: 'AES-GCM',
|
||||
iv: vector,
|
||||
}, key, dataBuffer).then(function (buf) {
|
||||
var buffer = new Uint8Array(buf)
|
||||
var vectorStr = encodeBufferToBase64(vector)
|
||||
var vaultStr = encodeBufferToBase64(buffer)
|
||||
return {
|
||||
data: vaultStr,
|
||||
iv: vectorStr,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Takes encrypted text, returns the restored Pojo.
|
||||
function decrypt (password, text) {
|
||||
const payload = JSON.parse(text)
|
||||
const salt = payload.salt
|
||||
return keyFromPassword(password + salt)
|
||||
.then(function (key) {
|
||||
return decryptWithKey(key, payload)
|
||||
})
|
||||
}
|
||||
|
||||
function decryptWithKey (key, payload) {
|
||||
const encryptedData = decodeBase64ToBuffer(payload.data)
|
||||
const vector = decodeBase64ToBuffer(payload.iv)
|
||||
return crypto.subtle.decrypt({name: 'AES-GCM', iv: vector}, key, encryptedData)
|
||||
.then(function (result) {
|
||||
const decryptedData = new Uint8Array(result)
|
||||
const decryptedStr = convertArrayBufferViewtoString(decryptedData)
|
||||
const decryptedObj = JSON.parse(decryptedStr)
|
||||
return decryptedObj
|
||||
})
|
||||
.catch(function (reason) {
|
||||
throw new Error('Incorrect password')
|
||||
})
|
||||
}
|
||||
|
||||
function convertStringToArrayBufferView (str) {
|
||||
var bytes = new Uint8Array(str.length)
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
bytes[i] = str.charCodeAt(i)
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
function convertArrayBufferViewtoString (buffer) {
|
||||
var str = ''
|
||||
for (var i = 0; i < buffer.byteLength; i++) {
|
||||
str += String.fromCharCode(buffer[i])
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
function keyFromPassword (password) {
|
||||
var passBuffer = convertStringToArrayBufferView(password)
|
||||
return global.crypto.subtle.digest('SHA-256', passBuffer)
|
||||
.then(function (passHash) {
|
||||
return global.crypto.subtle.importKey('raw', passHash, {name: 'AES-GCM'}, false, ['encrypt', 'decrypt'])
|
||||
})
|
||||
}
|
||||
|
||||
function serializeBufferFromStorage (str) {
|
||||
var stripStr = (str.slice(0, 2) === '0x') ? str.slice(2) : str
|
||||
var buf = new Uint8Array(stripStr.length / 2)
|
||||
for (var i = 0; i < stripStr.length; i += 2) {
|
||||
var seg = stripStr.substr(i, 2)
|
||||
buf[i / 2] = parseInt(seg, 16)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// Should return a string, ready for storage, in hex format.
|
||||
function serializeBufferForStorage (buffer) {
|
||||
var result = '0x'
|
||||
var len = buffer.length || buffer.byteLength
|
||||
for (var i = 0; i < len; i++) {
|
||||
result += unprefixedHex(buffer[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function unprefixedHex (num) {
|
||||
var hex = num.toString(16)
|
||||
while (hex.length < 2) {
|
||||
hex = '0' + hex
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
||||
function encodeBufferToBase64 (buf) {
|
||||
var b64encoded = btoa(String.fromCharCode.apply(null, buf))
|
||||
return b64encoded
|
||||
}
|
||||
|
||||
function decodeBase64ToBuffer (base64) {
|
||||
var buf = new Uint8Array(atob(base64).split('')
|
||||
.map(function (c) {
|
||||
return c.charCodeAt(0)
|
||||
}))
|
||||
return buf
|
||||
}
|
||||
|
||||
function generateSalt (byteCount = 32) {
|
||||
var view = new Uint8Array(byteCount)
|
||||
global.crypto.getRandomValues(view)
|
||||
var b64encoded = btoa(String.fromCharCode.apply(null, view))
|
||||
return b64encoded
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
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 {
|
||||
|
||||
@ -23,15 +26,13 @@ module.exports = class IdentityStoreMigrator {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.idStore.submitPassword(password, (err) => {
|
||||
if (err) return reject(err)
|
||||
try {
|
||||
resolve(this.serializeVault())
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
const idStore = this.idStore
|
||||
const submitPassword = denodeify(idStore.submitPassword.bind(idStore))
|
||||
|
||||
return submitPassword(password)
|
||||
.then(() => {
|
||||
const serialized = this.serializeVault()
|
||||
return this.checkForLostAccounts(serialized)
|
||||
})
|
||||
}
|
||||
|
||||
@ -45,6 +46,28 @@ module.exports = class IdentityStoreMigrator {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
hasOldVault () {
|
||||
const wallet = this.configManager.getWallet()
|
||||
return wallet
|
||||
|
@ -2,6 +2,7 @@ const extend = require('xtend')
|
||||
const EthStore = require('eth-store')
|
||||
const MetaMaskProvider = require('web3-provider-engine/zero.js')
|
||||
const KeyringController = require('./keyring-controller')
|
||||
const NoticeController = require('./notice-controller')
|
||||
const messageManager = require('./lib/message-manager')
|
||||
const TxManager = require('./transaction-manager')
|
||||
const HostStore = require('./lib/remote-store.js').HostStore
|
||||
@ -23,6 +24,13 @@ module.exports = class MetamaskController {
|
||||
txManager: this.txManager,
|
||||
getNetwork: this.getStateNetwork.bind(this),
|
||||
})
|
||||
// notices
|
||||
this.noticeController = new NoticeController({
|
||||
configManager: this.configManager,
|
||||
})
|
||||
this.noticeController.updateNoticesList()
|
||||
// to be uncommented when retrieving notices from a remote server.
|
||||
// this.noticeController.startPolling()
|
||||
this.provider = this.initializeProvider(opts)
|
||||
this.ethStore = new EthStore(this.provider)
|
||||
this.keyringController.setStore(this.ethStore)
|
||||
@ -57,12 +65,15 @@ module.exports = class MetamaskController {
|
||||
this.configManager.getConfig(),
|
||||
this.keyringController.getState(),
|
||||
this.txManager.getState()
|
||||
this.noticeController.getState()
|
||||
)
|
||||
}
|
||||
|
||||
getApi () {
|
||||
const keyringController = this.keyringController
|
||||
const txManager = this.txManager
|
||||
const noticeController = this.noticeController
|
||||
|
||||
return {
|
||||
getState: (cb) => { cb(null, this.getState()) },
|
||||
setRpcTarget: this.setRpcTarget.bind(this),
|
||||
@ -101,6 +112,9 @@ module.exports = class MetamaskController {
|
||||
buyEth: this.buyEth.bind(this),
|
||||
// shapeshift
|
||||
createShapeShiftTx: this.createShapeShiftTx.bind(this),
|
||||
// notices
|
||||
checkNotices: noticeController.updateNoticesList.bind(noticeController),
|
||||
markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,7 +306,7 @@ module.exports = class MetamaskController {
|
||||
setTOSHash (hash) {
|
||||
try {
|
||||
this.configManager.setTOSHash(hash)
|
||||
} catch (e) {
|
||||
} catch (err) {
|
||||
console.error('Error in setting terms of service hash.')
|
||||
}
|
||||
}
|
||||
@ -304,17 +318,19 @@ module.exports = class MetamaskController {
|
||||
this.resetDisclaimer()
|
||||
this.setTOSHash(global.TOS_HASH)
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (err) {
|
||||
console.error('Error in checking TOS change.')
|
||||
}
|
||||
}
|
||||
|
||||
// disclaimer
|
||||
|
||||
agreeToDisclaimer (cb) {
|
||||
try {
|
||||
this.configManager.setConfirmedDisclaimer(true)
|
||||
cb()
|
||||
} catch (e) {
|
||||
cb(e)
|
||||
} catch (err) {
|
||||
cb(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,8 +353,8 @@ module.exports = class MetamaskController {
|
||||
conversionDate: this.configManager.getConversionDate(),
|
||||
}
|
||||
cb(data)
|
||||
} catch (e) {
|
||||
cb(null, e)
|
||||
} catch (err) {
|
||||
cb(null, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,8 +427,8 @@ module.exports = class MetamaskController {
|
||||
try {
|
||||
this.configManager.setGasMultiplier(gasMultiplier)
|
||||
cb()
|
||||
} catch (e) {
|
||||
cb(e)
|
||||
} catch (err) {
|
||||
cb(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
96
app/scripts/notice-controller.js
Normal file
96
app/scripts/notice-controller.js
Normal file
@ -0,0 +1,96 @@
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const hardCodedNotices = require('../../development/notices.json')
|
||||
|
||||
module.exports = class NoticeController extends EventEmitter {
|
||||
|
||||
constructor (opts) {
|
||||
super()
|
||||
this.configManager = opts.configManager
|
||||
this.noticePoller = null
|
||||
}
|
||||
|
||||
getState () {
|
||||
var lastUnreadNotice = this.getLatestUnreadNotice()
|
||||
|
||||
return {
|
||||
lastUnreadNotice: lastUnreadNotice,
|
||||
noActiveNotices: !lastUnreadNotice,
|
||||
}
|
||||
}
|
||||
|
||||
getNoticesList () {
|
||||
var data = this.configManager.getData()
|
||||
if ('noticesList' in data) {
|
||||
return data.noticesList
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
setNoticesList (list) {
|
||||
var data = this.configManager.getData()
|
||||
data.noticesList = list
|
||||
this.configManager.setData(data)
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
markNoticeRead (notice, cb) {
|
||||
cb = cb || function (err) { if (err) throw err }
|
||||
try {
|
||||
var notices = this.getNoticesList()
|
||||
var id = notice.id
|
||||
notices[id].read = true
|
||||
this.setNoticesList(notices)
|
||||
const latestNotice = this.getLatestUnreadNotice()
|
||||
cb(null, latestNotice)
|
||||
} catch (err) {
|
||||
cb(err)
|
||||
}
|
||||
}
|
||||
|
||||
updateNoticesList () {
|
||||
return this._retrieveNoticeData().then((newNotices) => {
|
||||
var oldNotices = this.getNoticesList()
|
||||
var combinedNotices = this._mergeNotices(oldNotices, newNotices)
|
||||
return Promise.resolve(this.setNoticesList(combinedNotices))
|
||||
})
|
||||
}
|
||||
|
||||
getLatestUnreadNotice () {
|
||||
var notices = this.getNoticesList()
|
||||
var filteredNotices = notices.filter((notice) => {
|
||||
return notice.read === false
|
||||
})
|
||||
return filteredNotices[filteredNotices.length - 1]
|
||||
}
|
||||
|
||||
startPolling () {
|
||||
if (this.noticePoller) {
|
||||
clearInterval(this.noticePoller)
|
||||
}
|
||||
this.noticePoller = setInterval(() => {
|
||||
this.noticeController.updateNoticesList()
|
||||
}, 300000)
|
||||
}
|
||||
|
||||
_mergeNotices (oldNotices, newNotices) {
|
||||
var noticeMap = this._mapNoticeIds(oldNotices)
|
||||
newNotices.forEach((notice) => {
|
||||
if (noticeMap.indexOf(notice.id) === -1) {
|
||||
oldNotices.push(notice)
|
||||
}
|
||||
})
|
||||
return oldNotices
|
||||
}
|
||||
|
||||
_mapNoticeIds (notices) {
|
||||
return notices.map((notice) => notice.id)
|
||||
}
|
||||
|
||||
_retrieveNoticeData () {
|
||||
// Placeholder for the API.
|
||||
return Promise.resolve(hardCodedNotices)
|
||||
}
|
||||
|
||||
|
||||
}
|
36
development/notice-generator.js
Normal file
36
development/notice-generator.js
Normal file
@ -0,0 +1,36 @@
|
||||
var fsp = require('fs-promise')
|
||||
var path = require('path')
|
||||
var prompt = require('prompt')
|
||||
var open = require('open')
|
||||
var extend = require('extend')
|
||||
var notices = require('./notices.json')
|
||||
|
||||
var id = 0
|
||||
var date = new Date().toDateString()
|
||||
|
||||
var notice = {
|
||||
read: false,
|
||||
date: date,
|
||||
}
|
||||
|
||||
fsp.readdir('notices')
|
||||
.then((files) => {
|
||||
files.forEach(file => { id ++ })
|
||||
Promise.resolve()
|
||||
}).then(() => {
|
||||
fsp.writeFile(`notices/notice_${id}.md`,'Message goes here. Please write out your notice and save before proceeding at the command line.')
|
||||
.then(() => {
|
||||
open(`notices/notice_${id}.md`)
|
||||
prompt.start()
|
||||
prompt.get(['title'], (err, result) => {
|
||||
notice.title = result.title
|
||||
fsp.readFile(`notices/notice_${id}.md`)
|
||||
.then((body) => {
|
||||
notice.body = body.toString()
|
||||
notice.id = id
|
||||
notices.push(notice)
|
||||
return fsp.writeFile(`development/notices.json`, JSON.stringify(notices))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
1
development/notices.json
Normal file
1
development/notices.json
Normal file
@ -0,0 +1 @@
|
||||
[{"read":false,"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}]
|
File diff suppressed because one or more lines are too long
62
development/states/notice.json
Normal file
62
development/states/notice.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"metamask": {
|
||||
"isInitialized": true,
|
||||
"isUnlocked": true,
|
||||
"identities": {
|
||||
"0x24a1d059462456aa332d6da9117aa7f91a46f2ac": {
|
||||
"address": "0x24a1d059462456aa332d6da9117aa7f91a46f2ac",
|
||||
"name": "Account 1"
|
||||
}
|
||||
},
|
||||
"unconfTxs": {},
|
||||
"currentFiat": "USD",
|
||||
"conversionRate": 8.3533002,
|
||||
"conversionDate": 1481671082,
|
||||
"noActiveNotices": false,
|
||||
"lastUnreadNotice": {
|
||||
"read": false,
|
||||
"date": "Tue Dec 13 2016",
|
||||
"title": "MultiVault Support",
|
||||
"body": "# Multi\n# Line\n## Support\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi tincidunt dapibus justo a auctor. Sed luctus metus non mi laoreet, sit amet placerat nibh ultricies. Cras fringilla, urna sit amet sodales porttitor, lacus risus lacinia lorem, non euismod magna felis id ex. Nam iaculis, ante nec imperdiet suscipit, nisi quam fringilla nisl, sed fringilla turpis lectus et nibh. Pellentesque sed neque pretium nulla elementum lacinia eu eget felis. Nulla facilisi. Pellentesque id mi tempor, tempus sapien id, ultricies nibh. Integer faucibus elit non orci dapibus porttitor. Pellentesque rutrum hendrerit sapien ut lacinia. Nunc elementum eget arcu eu volutpat. Integer ullamcorper aliquam metus, eu malesuada tellus vestibulum a.\n",
|
||||
"id": 0
|
||||
},
|
||||
"network": "3",
|
||||
"accounts": {
|
||||
"0x24a1d059462456aa332d6da9117aa7f91a46f2ac": {
|
||||
"code": "0x",
|
||||
"nonce": "0x0",
|
||||
"balance": "0x0",
|
||||
"address": "0x24a1d059462456aa332d6da9117aa7f91a46f2ac"
|
||||
}
|
||||
},
|
||||
"transactions": [],
|
||||
"provider": {
|
||||
"type": "testnet"
|
||||
},
|
||||
"selectedAccount": "0x24a1d059462456aa332d6da9117aa7f91a46f2ac",
|
||||
"seedWords": null,
|
||||
"isDisclaimerConfirmed": true,
|
||||
"unconfMsgs": {},
|
||||
"messages": [],
|
||||
"shapeShiftTxList": [],
|
||||
"keyringTypes": [
|
||||
"Simple Key Pair",
|
||||
"HD Key Tree"
|
||||
]
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
"currentView": {
|
||||
"name": "accountDetail",
|
||||
"detailView": null,
|
||||
"context": "0x24a1d059462456aa332d6da9117aa7f91a46f2ac"
|
||||
},
|
||||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"transForward": true,
|
||||
"isLoading": false,
|
||||
"warning": null
|
||||
},
|
||||
"identities": {}
|
||||
}
|
40
development/states/terms-and-conditions.json
Normal file
40
development/states/terms-and-conditions.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"metamask": {
|
||||
"isInitialized": false,
|
||||
"isUnlocked": false,
|
||||
"rpcTarget": "https://rawtestrpc.metamask.io/",
|
||||
"identities": {},
|
||||
"unconfTxs": {},
|
||||
"currentFiat": "USD",
|
||||
"conversionRate": 8.18703468,
|
||||
"conversionDate": 1481755832,
|
||||
"network": "3",
|
||||
"accounts": {},
|
||||
"transactions": [],
|
||||
"provider": {
|
||||
"type": "testnet"
|
||||
},
|
||||
"isDisclaimerConfirmed": false,
|
||||
"unconfMsgs": {},
|
||||
"messages": [],
|
||||
"shapeShiftTxList": [],
|
||||
"keyringTypes": [
|
||||
"Simple Key Pair",
|
||||
"HD Key Tree"
|
||||
]
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
"currentView": {
|
||||
"name": "accounts",
|
||||
"detailView": null
|
||||
},
|
||||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"transForward": true,
|
||||
"isLoading": false,
|
||||
"warning": null
|
||||
},
|
||||
"identities": {}
|
||||
}
|
12
notices/notice_0.md
Normal file
12
notices/notice_0.md
Normal file
@ -0,0 +1,12 @@
|
||||
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.
|
||||
|
||||
Users 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/).
|
||||
|
||||
Please use the new Ropsten Network as your new default test network.
|
||||
|
||||
You can fund your Ropsten account using the buy button on your account page.
|
||||
|
||||
Best wishes!
|
||||
|
||||
The MetaMask Team
|
||||
|
18
package.json
18
package.json
@ -12,12 +12,14 @@
|
||||
"test": "npm run fastTest && npm run ci && npm run lint",
|
||||
"fastTest": "METAMASK_ENV=test mocha --require test/helper.js --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
|
||||
"watch": "mocha watch --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
|
||||
"ui": "node development/genStates.js && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
|
||||
"genStates": "node development/genStates.js",
|
||||
"ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
|
||||
"mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
|
||||
"buildMock": "browserify ./mock-dev.js -o ./development/bundle.js",
|
||||
"buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js",
|
||||
"testem": "npm run buildMock && testem",
|
||||
"ci": "npm run buildMock && npm run buildCiUnits && testem ci -P 2",
|
||||
"announce": "node development/announcer.js"
|
||||
"announce": "node development/announcer.js",
|
||||
"generateNotice": "node development/notice-generator.js"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
@ -35,10 +37,12 @@
|
||||
"dependencies": {
|
||||
"async": "^1.5.2",
|
||||
"bip39": "^2.2.0",
|
||||
"browser-passworder": "^2.0.3",
|
||||
"browserify-derequire": "^0.9.4",
|
||||
"clone": "^1.0.2",
|
||||
"copy-to-clipboard": "^2.0.0",
|
||||
"debounce": "^1.0.0",
|
||||
"denodeify": "^1.2.1",
|
||||
"dnode": "^1.2.2",
|
||||
"end-of-stream": "^1.1.0",
|
||||
"ensnare": "^1.0.0",
|
||||
@ -50,6 +54,8 @@
|
||||
"ethereumjs-util": "^4.4.0",
|
||||
"ethereumjs-wallet": "^0.6.0",
|
||||
"express": "^4.14.0",
|
||||
"extension-link-enabler": "^1.0.0",
|
||||
"extensionizer": "^1.0.0",
|
||||
"gulp-eslint": "^2.0.0",
|
||||
"hat": "0.0.3",
|
||||
"identicon.js": "^1.2.1",
|
||||
@ -85,9 +91,10 @@
|
||||
"textarea-caret": "^3.0.1",
|
||||
"three.js": "^0.73.2",
|
||||
"through2": "^2.0.1",
|
||||
"valid-url": "^1.0.9",
|
||||
"vreme": "^3.0.2",
|
||||
"web3": "0.17.0-beta",
|
||||
"web3-provider-engine": "^8.1.5",
|
||||
"web3-provider-engine": "^8.1.14",
|
||||
"web3-stream-provider": "^2.0.6",
|
||||
"xtend": "^4.0.1"
|
||||
},
|
||||
@ -101,6 +108,7 @@
|
||||
"chai": "^3.5.0",
|
||||
"deep-freeze-strict": "^1.1.1",
|
||||
"del": "^2.2.0",
|
||||
"fs-promise": "^1.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-brfs": "^0.1.0",
|
||||
"gulp-if": "^2.0.1",
|
||||
@ -120,6 +128,8 @@
|
||||
"mocha-jsdom": "^1.1.0",
|
||||
"mocha-sinon": "^1.1.5",
|
||||
"nock": "^8.0.0",
|
||||
"open": "0.0.5",
|
||||
"prompt": "^1.0.0",
|
||||
"qs": "^6.2.0",
|
||||
"qunit": "^0.9.1",
|
||||
"sinon": "^1.17.3",
|
||||
|
@ -1,71 +0,0 @@
|
||||
var encryptor = require('../../../app/scripts/lib/encryptor')
|
||||
|
||||
QUnit.module('encryptor')
|
||||
|
||||
QUnit.test('encryptor:serializeBufferForStorage', function (assert) {
|
||||
assert.expect(1)
|
||||
var buf = new Buffer(2)
|
||||
buf[0] = 16
|
||||
buf[1] = 1
|
||||
|
||||
var output = encryptor.serializeBufferForStorage(buf)
|
||||
|
||||
var expect = '0x1001'
|
||||
assert.equal(expect, output)
|
||||
})
|
||||
|
||||
QUnit.test('encryptor:serializeBufferFromStorage', function (assert) {
|
||||
assert.expect(2)
|
||||
var input = '0x1001'
|
||||
var output = encryptor.serializeBufferFromStorage(input)
|
||||
|
||||
assert.equal(output[0], 16)
|
||||
assert.equal(output[1], 1)
|
||||
})
|
||||
|
||||
QUnit.test('encryptor:encrypt & decrypt', function(assert) {
|
||||
var done = assert.async();
|
||||
var password, data, encrypted
|
||||
|
||||
password = 'a sample passw0rd'
|
||||
data = { foo: 'data to encrypt' }
|
||||
|
||||
encryptor.encrypt(password, data)
|
||||
.then(function(encryptedStr) {
|
||||
assert.equal(typeof encryptedStr, 'string', 'returns a string')
|
||||
return encryptor.decrypt(password, encryptedStr)
|
||||
})
|
||||
.then(function (decryptedObj) {
|
||||
assert.deepEqual(decryptedObj, data, 'decrypted what was encrypted')
|
||||
done()
|
||||
})
|
||||
.catch(function(reason) {
|
||||
assert.ifError(reason, 'threw an error')
|
||||
done(reason)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
QUnit.test('encryptor:encrypt & decrypt with wrong password', function(assert) {
|
||||
var done = assert.async();
|
||||
var password, data, encrypted, wrongPassword
|
||||
|
||||
password = 'a sample passw0rd'
|
||||
wrongPassword = 'a wrong password'
|
||||
data = { foo: 'data to encrypt' }
|
||||
|
||||
encryptor.encrypt(password, data)
|
||||
.then(function(encryptedStr) {
|
||||
assert.equal(typeof encryptedStr, 'string', 'returns a string')
|
||||
return encryptor.decrypt(wrongPassword, encryptedStr)
|
||||
})
|
||||
.then(function (decryptedObj) {
|
||||
assert.equal(!decryptedObj, true, 'Wrong password should not decrypt')
|
||||
done()
|
||||
})
|
||||
.catch(function(reason) {
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -2,11 +2,14 @@ var KeyringController = require('../../../app/scripts/keyring-controller')
|
||||
var ConfigManager = require('../../../app/scripts/lib/config-manager')
|
||||
|
||||
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 BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9'
|
||||
|
||||
|
||||
QUnit.module('Old Style Vaults', {
|
||||
beforeEach: function () {
|
||||
@ -45,6 +48,63 @@ QUnit.test('keyringController:submitPassword', function (assert) {
|
||||
this.keyringController.submitPassword(PASSWORD)
|
||||
.then((state) => {
|
||||
assert.ok(state.identities[FIRST_ADDRESS])
|
||||
assert.equal(state.lostAccounts.length, 0, 'no lost accounts')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.test('keyringController:setLocked', function (assert) {
|
||||
var done = assert.async()
|
||||
var self = this
|
||||
|
||||
this.keyringController.setLocked()
|
||||
.then(function() {
|
||||
assert.notOk(self.keyringController.password, 'password should be deallocated')
|
||||
assert.deepEqual(self.keyringController.keyrings, [], 'keyrings should be deallocated')
|
||||
done()
|
||||
})
|
||||
.catch((reason) => {
|
||||
assert.ifError(reason)
|
||||
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.keyringController = new KeyringController({
|
||||
configManager: this.configManager,
|
||||
getNetwork: () => { return '2' },
|
||||
})
|
||||
|
||||
this.ethStore = {
|
||||
addAccount: () => {},
|
||||
removeAccount: () => {},
|
||||
}
|
||||
|
||||
this.keyringController.setStore(this.ethStore)
|
||||
}
|
||||
})
|
||||
|
||||
QUnit.test('keyringController:isInitialized', function (assert) {
|
||||
assert.ok(this.keyringController.getState().isInitialized, 'vault is initialized')
|
||||
})
|
||||
|
||||
QUnit.test('keyringController:submitPassword', function (assert) {
|
||||
var done = assert.async()
|
||||
|
||||
this.keyringController.submitPassword(PASSWORD)
|
||||
.then((state) => {
|
||||
assert.ok(state.identities[BAD_STYLE_FIRST_ADDRESS])
|
||||
assert.equal(state.lostAccounts.length, 1, 'one lost account')
|
||||
assert.equal(state.lostAccounts[0], '0xe15D894BeCB0354c501AE69429B05143679F39e0'.toLowerCase())
|
||||
assert.deepEqual(this.configManager.getLostAccounts(), state.lostAccounts, 'persisted')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
1
test/integration/mocks/badVault.json
Normal file
1
test/integration/mocks/badVault.json
Normal file
@ -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}}
|
@ -1,12 +1,12 @@
|
||||
const assert = require('assert')
|
||||
const extend = require('xtend')
|
||||
const STORAGE_KEY = 'metamask-persistance-key'
|
||||
var configManagerGen = require('../lib/mock-config-manager')
|
||||
var configManager
|
||||
const rp = require('request-promise')
|
||||
const nock = require('nock')
|
||||
var configManagerGen = require('../lib/mock-config-manager')
|
||||
const STORAGE_KEY = 'metamask-persistance-key'
|
||||
|
||||
describe('config-manager', function() {
|
||||
var configManager
|
||||
|
||||
beforeEach(function() {
|
||||
window.localStorage = {} // Hacking localStorage support into JSDom
|
||||
|
@ -21,6 +21,10 @@ const mockVault = {
|
||||
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:
|
||||
@ -83,33 +87,8 @@ describe('IdentityStore to KeyringController migration', function() {
|
||||
keyringController.configManager.setWallet('something')
|
||||
const state = keyringController.getState()
|
||||
assert(state.isInitialized, 'old vault counted as initialized.')
|
||||
assert.equal(state.lostAccounts.length, 0, 'no lost accounts')
|
||||
})
|
||||
|
||||
/*
|
||||
it('should use the password to migrate the old vault', function(done) {
|
||||
this.timeout(5000)
|
||||
console.log('calling submitPassword')
|
||||
console.dir(keyringController)
|
||||
keyringController.submitPassword(password, function (err, state) {
|
||||
assert.ifError(err, 'submitPassword threw error')
|
||||
|
||||
function log(str, dat) { console.log(str + ': ' + JSON.stringify(dat)) }
|
||||
|
||||
let newAccounts = keyringController.getAccounts()
|
||||
log('new accounts: ', newAccounts)
|
||||
|
||||
let newAccount = ethUtil.addHexPrefix(newAccounts[0])
|
||||
assert.equal(ethUtil.addHexPrefix(newAccount), mockVault.account, 'restored the correct account')
|
||||
const newSeed = keyringController.keyrings[0].mnemonic
|
||||
log('keyringController keyrings', keyringController.keyrings)
|
||||
assert.equal(newSeed, mockVault.seed, 'seed phrase transferred.')
|
||||
|
||||
assert(configManager.getVault(), 'new type of vault is persisted')
|
||||
done()
|
||||
})
|
||||
})
|
||||
*/
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -99,21 +99,6 @@ describe('KeyringController', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#migrateOldVaultIfAny', function() {
|
||||
it('should return and init a new vault', function(done) {
|
||||
keyringController.migrateOldVaultIfAny(password)
|
||||
.then(() => {
|
||||
assert(keyringController.configManager.getVault(), 'now has a vault')
|
||||
assert(keyringController.password, 'has a password set')
|
||||
done()
|
||||
})
|
||||
.catch((reason) => {
|
||||
assert.ifError(reason)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createNickname', function() {
|
||||
it('should add the address to the identities hash', function() {
|
||||
const fakeAddress = '0x12345678'
|
||||
|
115
test/unit/notice-controller-test.js
Normal file
115
test/unit/notice-controller-test.js
Normal file
@ -0,0 +1,115 @@
|
||||
const assert = require('assert')
|
||||
const extend = require('xtend')
|
||||
const rp = require('request-promise')
|
||||
const nock = require('nock')
|
||||
const configManagerGen = require('../lib/mock-config-manager')
|
||||
const NoticeController = require('../../app/scripts/notice-controller')
|
||||
const STORAGE_KEY = 'metamask-persistance-key'
|
||||
// Hacking localStorage support into JSDom
|
||||
window.localStorage = {}
|
||||
|
||||
describe('notice-controller', function() {
|
||||
var noticeController
|
||||
|
||||
beforeEach(function() {
|
||||
let configManager = configManagerGen()
|
||||
noticeController = new NoticeController({
|
||||
configManager: configManager,
|
||||
})
|
||||
})
|
||||
|
||||
describe('notices', function() {
|
||||
describe('#getNoticesList', function() {
|
||||
it('should return an empty array when new', function() {
|
||||
var testList = [{
|
||||
id:0,
|
||||
read:false,
|
||||
title:"Futuristic Notice"
|
||||
}]
|
||||
var result = noticeController.getNoticesList()
|
||||
assert.equal(result.length, 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#setNoticesList', function() {
|
||||
it('should set data appropriately', function () {
|
||||
var testList = [{
|
||||
id:0,
|
||||
read:false,
|
||||
title:"Futuristic Notice"
|
||||
}]
|
||||
noticeController.setNoticesList(testList)
|
||||
var testListId = noticeController.getNoticesList()[0].id
|
||||
assert.equal(testListId, 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#updateNoticeslist', function() {
|
||||
it('should integrate the latest changes from the source', function() {
|
||||
var testList = [{
|
||||
id:55,
|
||||
read:false,
|
||||
title:"Futuristic Notice"
|
||||
}]
|
||||
noticeController.setNoticesList(testList)
|
||||
noticeController.updateNoticesList().then(() => {
|
||||
var newList = noticeController.getNoticesList()
|
||||
assert.ok(newList[0].id === 55)
|
||||
assert.ok(newList[1])
|
||||
})
|
||||
})
|
||||
it('should not overwrite any existing fields', function () {
|
||||
var testList = [{
|
||||
id:0,
|
||||
read:false,
|
||||
title:"Futuristic Notice"
|
||||
}]
|
||||
noticeController.setNoticesList(testList)
|
||||
noticeController.updateNoticesList().then(() => {
|
||||
var newList = noticeController.getNoticesList()
|
||||
assert.equal(newList[0].id, 0)
|
||||
assert.equal(newList[0].title, "Futuristic Notice")
|
||||
assert.equal(newList.length, 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#markNoticeRead', function () {
|
||||
it('should mark a notice as read', function () {
|
||||
var testList = [{
|
||||
id:0,
|
||||
read:false,
|
||||
title:"Futuristic Notice"
|
||||
}]
|
||||
noticeController.setNoticesList(testList)
|
||||
noticeController.markNoticeRead(testList[0])
|
||||
var newList = noticeController.getNoticesList()
|
||||
assert.ok(newList[0].read)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getLatestUnreadNotice', function () {
|
||||
it('should retrieve the latest unread notice', function () {
|
||||
var testList = [
|
||||
{id:0,read:true,title:"Past Notice"},
|
||||
{id:1,read:false,title:"Current Notice"},
|
||||
{id:2,read:false,title:"Future Notice"},
|
||||
]
|
||||
noticeController.setNoticesList(testList)
|
||||
var latestUnread = noticeController.getLatestUnreadNotice()
|
||||
assert.equal(latestUnread.id, 2)
|
||||
})
|
||||
it('should return undefined if no unread notices exist.', function () {
|
||||
var testList = [
|
||||
{id:0,read:true,title:"Past Notice"},
|
||||
{id:1,read:true,title:"Current Notice"},
|
||||
{id:2,read:true,title:"Future Notice"},
|
||||
]
|
||||
noticeController.setNoticesList(testList)
|
||||
var latestUnread = noticeController.getLatestUnreadNotice()
|
||||
assert.ok(!latestUnread)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
@ -13,12 +13,21 @@ var actions = {
|
||||
// remote state
|
||||
UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
|
||||
updateMetamaskState: updateMetamaskState,
|
||||
// notices
|
||||
MARK_NOTICE_READ: 'MARK_NOTICE_READ',
|
||||
markNoticeRead: markNoticeRead,
|
||||
SHOW_NOTICE: 'SHOW_NOTICE',
|
||||
showNotice: showNotice,
|
||||
CLEAR_NOTICES: 'CLEAR_NOTICES',
|
||||
clearNotices: clearNotices,
|
||||
// intialize screen
|
||||
AGREE_TO_DISCLAIMER: 'AGREE_TO_DISCLAIMER',
|
||||
agreeToDisclaimer: agreeToDisclaimer,
|
||||
CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS',
|
||||
SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT',
|
||||
SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT',
|
||||
FORGOT_PASSWORD: 'FORGOT_PASSWORD',
|
||||
forgotPassword: forgotPassword,
|
||||
SHOW_INIT_MENU: 'SHOW_INIT_MENU',
|
||||
SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED',
|
||||
SHOW_INFO_PAGE: 'SHOW_INFO_PAGE',
|
||||
@ -177,13 +186,13 @@ function tryUnlockMetamask (password) {
|
||||
}
|
||||
}
|
||||
|
||||
function transitionForward() {
|
||||
function transitionForward () {
|
||||
return {
|
||||
type: this.TRANSITION_FORWARD,
|
||||
}
|
||||
}
|
||||
|
||||
function transitionBackward() {
|
||||
function transitionBackward () {
|
||||
return {
|
||||
type: this.TRANSITION_BACKWARD,
|
||||
}
|
||||
@ -380,6 +389,12 @@ function showRestoreVault () {
|
||||
}
|
||||
}
|
||||
|
||||
function forgotPassword () {
|
||||
return {
|
||||
type: actions.FORGOT_PASSWORD,
|
||||
}
|
||||
}
|
||||
|
||||
function showInitializeMenu () {
|
||||
return {
|
||||
type: actions.SHOW_INIT_MENU,
|
||||
@ -539,6 +554,43 @@ function goBackToInitView () {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// notice
|
||||
//
|
||||
|
||||
function markNoticeRead (notice) {
|
||||
return (dispatch) => {
|
||||
dispatch(this.showLoadingIndication())
|
||||
background.markNoticeRead(notice, (err, notice) => {
|
||||
dispatch(this.hideLoadingIndication())
|
||||
if (err) {
|
||||
return dispatch(actions.showWarning(err))
|
||||
}
|
||||
if (notice) {
|
||||
return dispatch(actions.showNotice(notice))
|
||||
} else {
|
||||
dispatch(this.clearNotices())
|
||||
return {
|
||||
type: actions.SHOW_ACCOUNTS_PAGE,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function showNotice (notice) {
|
||||
return {
|
||||
type: actions.SHOW_NOTICE,
|
||||
value: notice,
|
||||
}
|
||||
}
|
||||
|
||||
function clearNotices () {
|
||||
return {
|
||||
type: actions.CLEAR_NOTICES,
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// config
|
||||
//
|
||||
|
@ -15,6 +15,8 @@ const AccountsScreen = require('./accounts')
|
||||
const AccountDetailScreen = require('./account-detail')
|
||||
const SendTransactionScreen = require('./send')
|
||||
const ConfirmTxScreen = require('./conf-tx')
|
||||
// notice
|
||||
const NoticeScreen = require('./notice')
|
||||
// other views
|
||||
const ConfigScreen = require('./config')
|
||||
const InfoScreen = require('./info')
|
||||
@ -40,6 +42,7 @@ function mapStateToProps (state) {
|
||||
// state from plugin
|
||||
isLoading: state.appState.isLoading,
|
||||
isDisclaimerConfirmed: state.metamask.isDisclaimerConfirmed,
|
||||
noActiveNotices: state.metamask.noActiveNotices,
|
||||
isInitialized: state.metamask.isInitialized,
|
||||
isUnlocked: state.metamask.isUnlocked,
|
||||
currentView: state.appState.currentView,
|
||||
@ -240,15 +243,6 @@ App.prototype.renderNetworkDropdown = function () {
|
||||
provider: props.provider,
|
||||
}),
|
||||
|
||||
h(DropMenuItem, {
|
||||
label: 'Morden Test Network',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
|
||||
action: () => props.dispatch(actions.setProviderType('morden')),
|
||||
icon: h('.menu-icon.red-dot'),
|
||||
activeNetworkRender: props.network,
|
||||
provider: props.provider,
|
||||
}),
|
||||
|
||||
h(DropMenuItem, {
|
||||
label: 'Localhost 8545',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
|
||||
@ -372,6 +366,10 @@ App.prototype.renderPrimary = function () {
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.noActiveNotices) {
|
||||
return h(NoticeScreen, {key: 'NoticeScreen'})
|
||||
}
|
||||
|
||||
// show current view
|
||||
switch (props.currentView.name) {
|
||||
|
||||
|
@ -44,9 +44,6 @@ DropMenuItem.prototype.activeNetworkRender = function () {
|
||||
case 'Ropsten Test Network':
|
||||
if (provider.type === 'testnet') return h('.check', '✓')
|
||||
break
|
||||
case 'Morden Test Network':
|
||||
if (provider.type === 'morden') return h('.check', '✓')
|
||||
break
|
||||
case 'Localhost 8545':
|
||||
if (activeNetwork === 'http://localhost:8545') return h('.check', '✓')
|
||||
break
|
||||
|
@ -40,9 +40,6 @@ Network.prototype.render = function () {
|
||||
} else if (parseInt(networkNumber) === 3) {
|
||||
hoverText = 'Ropsten Test Network'
|
||||
iconName = 'ropsten-test-network'
|
||||
} else if (parseInt(networkNumber) === 2) {
|
||||
hoverText = 'Morden Test Network'
|
||||
iconName = 'morden-test-network'
|
||||
} else {
|
||||
hoverText = 'Unknown Private Network'
|
||||
iconName = 'unknown-private-network'
|
||||
@ -77,15 +74,6 @@ Network.prototype.render = function () {
|
||||
}},
|
||||
'Ropsten Test Net'),
|
||||
])
|
||||
case 'morden-test-network':
|
||||
return h('.network-indicator', [
|
||||
h('.menu-icon.red-dot'),
|
||||
h('.network-name', {
|
||||
style: {
|
||||
color: '#ff6666',
|
||||
}},
|
||||
'Morden Test Net'),
|
||||
])
|
||||
default:
|
||||
return h('.network-indicator', [
|
||||
h('i.fa.fa-question-circle.fa-lg', {
|
||||
|
@ -4,11 +4,13 @@ const h = require('react-hyperscript')
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('./actions')
|
||||
const currencies = require('./conversion.json').rows
|
||||
const validUrl = require('valid-url')
|
||||
module.exports = connect(mapStateToProps)(ConfigScreen)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
metamask: state.metamask,
|
||||
warning: state.appState.warning,
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +22,7 @@ function ConfigScreen () {
|
||||
ConfigScreen.prototype.render = function () {
|
||||
var state = this.props
|
||||
var metamaskState = state.metamask
|
||||
var warning = state.warning
|
||||
|
||||
return (
|
||||
h('.flex-column.flex-grow', [
|
||||
@ -34,6 +37,14 @@ ConfigScreen.prototype.render = function () {
|
||||
h('h2.page-subtitle', 'Settings'),
|
||||
]),
|
||||
|
||||
h('.error', {
|
||||
style: {
|
||||
display: warning ? 'block' : 'none',
|
||||
padding: '0 20px',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}, warning),
|
||||
|
||||
// conf view
|
||||
h('.flex-column.flex-justify-center.flex-grow.select-none', [
|
||||
h('.flex-space-around', {
|
||||
@ -57,7 +68,7 @@ ConfigScreen.prototype.render = function () {
|
||||
if (event.key === 'Enter') {
|
||||
var element = event.target
|
||||
var newRpc = element.value
|
||||
state.dispatch(actions.setRpcTarget(newRpc))
|
||||
rpcValidation(newRpc, state)
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -69,7 +80,7 @@ ConfigScreen.prototype.render = function () {
|
||||
event.preventDefault()
|
||||
var element = document.querySelector('input#new_rpc')
|
||||
var newRpc = element.value
|
||||
state.dispatch(actions.setRpcTarget(newRpc))
|
||||
rpcValidation(newRpc, state)
|
||||
},
|
||||
}, 'Save'),
|
||||
]),
|
||||
@ -99,6 +110,19 @@ ConfigScreen.prototype.render = function () {
|
||||
)
|
||||
}
|
||||
|
||||
function rpcValidation (newRpc, state) {
|
||||
if (validUrl.isWebUri(newRpc)) {
|
||||
state.dispatch(actions.setRpcTarget(newRpc))
|
||||
} else {
|
||||
var appendedRpc = `http://${newRpc}`
|
||||
if (validUrl.isWebUri(appendedRpc)) {
|
||||
state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.'))
|
||||
} else {
|
||||
state.dispatch(actions.displayWarning('Invalid RPC URI'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function currentConversionInformation (metamaskState, state) {
|
||||
var currentFiat = metamaskState.currentFiat
|
||||
var conversionDate = metamaskState.conversionDate
|
||||
@ -133,7 +157,7 @@ function currentProviderDisplay (metamaskState) {
|
||||
|
||||
case 'testnet':
|
||||
title = 'Current Network'
|
||||
value = 'Morden Test Network'
|
||||
value = 'Ropsten Test Network'
|
||||
break
|
||||
|
||||
default:
|
||||
|
@ -6,6 +6,8 @@ const actions = require('../actions')
|
||||
const ReactMarkdown = require('react-markdown')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const linker = require('extension-link-enabler')
|
||||
const findDOMNode = require('react-dom').findDOMNode
|
||||
const disclaimer = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'USER_AGREEMENT.md')).toString()
|
||||
module.exports = connect(mapStateToProps)(DisclaimerScreen)
|
||||
|
||||
@ -98,3 +100,13 @@ DisclaimerScreen.prototype.render = function () {
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
DisclaimerScreen.prototype.componentDidMount = function () {
|
||||
var node = findDOMNode(this)
|
||||
linker.setupListener(node)
|
||||
}
|
||||
|
||||
DisclaimerScreen.prototype.componentWillUnmount = function () {
|
||||
var node = findDOMNode(this)
|
||||
linker.teardownListener(node)
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ function mapStateToProps (state) {
|
||||
// state from plugin
|
||||
currentView: state.appState.currentView,
|
||||
warning: state.appState.warning,
|
||||
forgottenPassword: state.metamask.isInitialized,
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,17 +117,6 @@ InitializeMenuScreen.prototype.renderMenu = function (state) {
|
||||
},
|
||||
}, 'Create'),
|
||||
|
||||
state.forgottenPassword ? h('.flex-row.flex-center.flex-grow', [
|
||||
h('p.pointer', {
|
||||
onClick: this.backToUnlockView.bind(this),
|
||||
style: {
|
||||
fontSize: '0.8em',
|
||||
color: 'rgb(247, 134, 28)',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}, 'Return to Login'),
|
||||
]) : null,
|
||||
|
||||
h('.flex-row.flex-center.flex-grow', [
|
||||
h('p.pointer', {
|
||||
onClick: this.showRestoreVault.bind(this),
|
||||
@ -159,10 +147,6 @@ InitializeMenuScreen.prototype.showRestoreVault = function () {
|
||||
this.props.dispatch(actions.showRestoreVault())
|
||||
}
|
||||
|
||||
InitializeMenuScreen.prototype.backToUnlockView = function () {
|
||||
this.props.dispatch(actions.backToUnlockView())
|
||||
}
|
||||
|
||||
InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () {
|
||||
var passwordBox = document.getElementById('password-box')
|
||||
var password = passwordBox.value
|
||||
|
@ -14,6 +14,7 @@ function RestoreVaultScreen () {
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
warning: state.appState.warning,
|
||||
forgottenPassword: state.appState.forgottenPassword,
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,14 +101,17 @@ RestoreVaultScreen.prototype.render = function () {
|
||||
}, 'OK'),
|
||||
|
||||
]),
|
||||
|
||||
])
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
RestoreVaultScreen.prototype.showInitializeMenu = function () {
|
||||
this.props.dispatch(actions.showInitializeMenu())
|
||||
if (this.props.forgottenPassword) {
|
||||
this.props.dispatch(actions.backToUnlockView())
|
||||
} else {
|
||||
this.props.dispatch(actions.showInitializeMenu())
|
||||
}
|
||||
}
|
||||
|
||||
RestoreVaultScreen.prototype.createOnEnter = function (event) {
|
||||
|
118
ui/app/notice.js
Normal file
118
ui/app/notice.js
Normal file
@ -0,0 +1,118 @@
|
||||
const inherits = require('util').inherits
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const ReactMarkdown = require('react-markdown')
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('./actions')
|
||||
const linker = require('extension-link-enabler')
|
||||
const findDOMNode = require('react-dom').findDOMNode
|
||||
|
||||
module.exports = connect(mapStateToProps)(Notice)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
lastUnreadNotice: state.metamask.lastUnreadNotice,
|
||||
}
|
||||
}
|
||||
|
||||
inherits(Notice, Component)
|
||||
function Notice () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
Notice.prototype.render = function () {
|
||||
const props = this.props
|
||||
const title = props.lastUnreadNotice.title
|
||||
const date = props.lastUnreadNotice.date
|
||||
|
||||
return (
|
||||
h('.flex-column.flex-center.flex-grow', [
|
||||
h('h3.flex-center.text-transform-uppercacse.terms-header', {
|
||||
style: {
|
||||
background: '#EBEBEB',
|
||||
color: '#AEAEAE',
|
||||
width: '100%',
|
||||
fontSize: '20px',
|
||||
textAlign: 'center',
|
||||
padding: 6,
|
||||
},
|
||||
}, [
|
||||
title,
|
||||
]),
|
||||
|
||||
h('h5.flex-center.text-transform-uppercacse.terms-header', {
|
||||
style: {
|
||||
background: '#EBEBEB',
|
||||
color: '#AEAEAE',
|
||||
marginBottom: 24,
|
||||
width: '100%',
|
||||
fontSize: '20px',
|
||||
textAlign: 'center',
|
||||
padding: 6,
|
||||
},
|
||||
}, [
|
||||
date,
|
||||
]),
|
||||
|
||||
h('style', `
|
||||
|
||||
.markdown {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.markdown h1, .markdown h2, .markdown h3 {
|
||||
margin: 10px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.markdown strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
color: #df6b0e;
|
||||
}
|
||||
|
||||
`),
|
||||
|
||||
h('div.markdown', {
|
||||
style: {
|
||||
background: 'rgb(235, 235, 235)',
|
||||
height: '310px',
|
||||
padding: '6px',
|
||||
width: '90%',
|
||||
overflowY: 'scroll',
|
||||
scroll: 'auto',
|
||||
},
|
||||
}, [
|
||||
h(ReactMarkdown, {
|
||||
source: props.lastUnreadNotice.body,
|
||||
skipHtml: true,
|
||||
}),
|
||||
]),
|
||||
|
||||
h('button', {
|
||||
onClick: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
|
||||
style: {
|
||||
marginTop: '18px',
|
||||
},
|
||||
}, 'Continue'),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
Notice.prototype.componentDidMount = function () {
|
||||
var node = findDOMNode(this)
|
||||
linker.setupListener(node)
|
||||
}
|
||||
|
||||
Notice.prototype.componentWillUnmount = function () {
|
||||
var node = findDOMNode(this)
|
||||
linker.teardownListener(node)
|
||||
}
|
@ -72,6 +72,16 @@ function reduceApp (state, action) {
|
||||
name: 'restoreVault',
|
||||
},
|
||||
transForward: true,
|
||||
forgottenPassword: true,
|
||||
})
|
||||
|
||||
case actions.FORGOT_PASSWORD:
|
||||
return extend(appState, {
|
||||
currentView: {
|
||||
name: 'restoreVault',
|
||||
},
|
||||
transForward: false,
|
||||
forgottenPassword: true,
|
||||
})
|
||||
|
||||
case actions.SHOW_INIT_MENU:
|
||||
@ -169,7 +179,7 @@ function reduceApp (state, action) {
|
||||
return extend(appState, {
|
||||
warning: null,
|
||||
transForward: true,
|
||||
forgottenPassword: !appState.forgottenPassword,
|
||||
forgottenPassword: false,
|
||||
currentView: {
|
||||
name: 'UnlockScreen',
|
||||
},
|
||||
@ -248,6 +258,12 @@ function reduceApp (state, action) {
|
||||
forgottenPassword: false,
|
||||
})
|
||||
|
||||
case actions.SHOW_NOTICE:
|
||||
return extend(appState, {
|
||||
transForward: true,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
case actions.REVEAL_ACCOUNT:
|
||||
return extend(appState, {
|
||||
scrollToBottom: true,
|
||||
|
@ -16,6 +16,8 @@ function reduceMetamask (state, action) {
|
||||
currentFiat: 'USD',
|
||||
conversionRate: 0,
|
||||
conversionDate: 'N/A',
|
||||
noActiveNotices: true,
|
||||
lastUnreadNotice: undefined,
|
||||
}, state.metamask)
|
||||
|
||||
switch (action.type) {
|
||||
@ -25,6 +27,17 @@ function reduceMetamask (state, action) {
|
||||
delete newState.seedWords
|
||||
return newState
|
||||
|
||||
case actions.SHOW_NOTICE:
|
||||
return extend(metamaskState, {
|
||||
noActiveNotices: false,
|
||||
lastUnreadNotice: action.value,
|
||||
})
|
||||
|
||||
case actions.CLEAR_NOTICES:
|
||||
return extend(metamaskState, {
|
||||
noActiveNotices: true,
|
||||
})
|
||||
|
||||
case actions.UPDATE_METAMASK_STATE:
|
||||
return extend(metamaskState, action.value)
|
||||
|
||||
|
@ -70,7 +70,7 @@ UnlockScreen.prototype.render = function () {
|
||||
|
||||
h('.flex-row.flex-center.flex-grow', [
|
||||
h('p.pointer', {
|
||||
onClick: () => this.props.dispatch(actions.goBackToInitView()),
|
||||
onClick: () => this.props.dispatch(actions.forgotPassword()),
|
||||
style: {
|
||||
fontSize: '0.8em',
|
||||
color: 'rgb(247, 134, 28)',
|
||||
|
Loading…
Reference in New Issue
Block a user