Merge pull request #1307 from MetaMask/mascara

Proof of Concept: Mascara
This commit is contained in:
kumavis 2017-04-04 11:27:45 -07:00 committed by GitHub
commit 5d967eeebb
18 changed files with 328 additions and 220 deletions

View File

@ -212,7 +212,6 @@ module.exports = class MetamaskController extends EventEmitter {
//
getState () {
const wallet = this.configManager.getWallet()
const vault = this.keyringController.store.getState().vault
const isInitialized = (!!wallet || !!vault)

View File

@ -1,24 +0,0 @@
start the dual servers (dapp + mascara)
```
node server.js
```
open the example dapp at `http://localhost:9002/`
*You will need to build MetaMask in order for this to work*
```
gulp dev
```
to build MetaMask and have it live reload if you make changes
## First time use:
- navigate to: http://127.0.0.1:9001/popup/popup.html
- Create an Account
- go back to http://localhost:9002/
- open devTools
- click Sync Tx
### Todos
- Look into using [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)

View File

@ -1,159 +0,0 @@
const urlUtil = require('url')
const extend = require('xtend')
const Dnode = require('dnode')
const eos = require('end-of-stream')
const ParentStream = require('iframe-stream').ParentStream
const PortStream = require('../app/scripts/lib/port-stream.js')
const notification = require('../app/scripts/lib/notifications.js')
const messageManager = require('../app/scripts/lib/message-manager')
const setupMultiplex = require('../app/scripts/lib/stream-utils.js').setupMultiplex
const MetamaskController = require('../app/scripts/metamask-controller')
const extension = require('../app/scripts/lib/extension')
const STORAGE_KEY = 'metamask-config'
initializeZeroClient()
function initializeZeroClient() {
const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage,
unlockAccountMessage,
showUnapprovedTx,
// Persistence Methods:
setData,
loadData,
})
const idStore = controller.idStore
function unlockAccountMessage () {
console.log('notif stub - unlockAccountMessage')
}
function showUnconfirmedMessage (msgParams, msgId) {
console.log('notif stub - showUnconfirmedMessage')
}
function showUnapprovedTx (txParams, txData, onTxDoneCb) {
console.log('notif stub - showUnapprovedTx')
}
//
// connect to other contexts
//
var connectionStream = new ParentStream()
connectRemote(connectionStream, getParentHref())
function connectRemote (connectionStream, originDomain) {
var isMetaMaskInternalProcess = (originDomain === '127.0.0.1:9001')
if (isMetaMaskInternalProcess) {
// communication with popup
setupTrustedCommunication(connectionStream, 'MetaMask')
} else {
// communication with page
setupUntrustedCommunication(connectionStream, originDomain)
}
}
function setupUntrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
controller.setupPublicConfig(mx.createStream('publicConfig'))
}
function setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
setupControllerConnection(mx.createStream('controller'))
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
}
//
// remote features
//
function setupControllerConnection (stream) {
controller.stream = stream
var api = controller.getApi()
var dnode = Dnode(api)
stream.pipe(dnode).pipe(stream)
dnode.on('remote', (remote) => {
// push updates to popup
controller.ethStore.on('update', controller.sendUpdate.bind(controller))
controller.listeners.push(remote)
idStore.on('update', controller.sendUpdate.bind(controller))
// teardown on disconnect
eos(stream, () => {
controller.ethStore.removeListener('update', controller.sendUpdate.bind(controller))
})
})
}
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 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
}
function setData (data) {
window.localStorage[STORAGE_KEY] = JSON.stringify(data)
}
function getParentHref(){
try {
var parentLocation = window.parent.location
return parentLocation.hostname + ':' + parentLocation.port
} catch (err) {
return 'unknown'
}
}
}

View File

@ -1,19 +0,0 @@
const injectCss = require('inject-css')
const MetaMaskUiCss = require('../ui/css')
const startPopup = require('../app/scripts/popup-core')
const setupIframe = require('./lib/setup-iframe.js')
var css = MetaMaskUiCss()
injectCss(css)
var name = 'popup'
window.METAMASK_UI_TYPE = name
var iframeStream = setupIframe({
zeroClientProvider: 'http://127.0.0.1:9001',
sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'],
container: document.body,
})
startPopup(iframeStream)

20
mascara/README.md Normal file
View File

@ -0,0 +1,20 @@
start the dual servers (dapp + mascara)
```
node server.js
```
## First time use:
- navigate to: http://localhost:9001/popup/popup.html
- Create an Account
- go back to http://localhost:9002/
- open devTools
- click Sync Tx
### Todos
- [ ] Figure out user flows and UI redesign
- [ ] Figure out FireFox
Standing problems:
- [ ] IndexDb

View File

@ -1,5 +1,5 @@
window.addEventListener('load', web3Detect)
window.addEventListener('message', console.warn)
function web3Detect() {
if (global.web3) {
@ -13,10 +13,10 @@ function web3Detect() {
function startApp(){
console.log('app started')
var primaryAccount = null
var primaryAccount
console.log('getting main account...')
web3.eth.getAccounts(function(err, addresses){
if (err) throw err
web3.eth.getAccounts((err, addresses) => {
if (err) console.error(err)
console.log('set address', addresses[0])
primaryAccount = addresses[0]
})
@ -24,6 +24,7 @@ function startApp(){
document.querySelector('.action-button-1').addEventListener('click', function(){
console.log('saw click')
console.log('sending tx')
primaryAccount
web3.eth.sendTransaction({
from: primaryAccount,
to: primaryAccount,
@ -53,4 +54,4 @@ function startApp(){
function logToDom(message){
document.body.appendChild(document.createTextNode(message))
console.log(message)
}
}

View File

@ -3,9 +3,11 @@ const browserify = require('browserify')
const watchify = require('watchify')
const babelify = require('babelify')
const zeroBundle = createBundle('./index.js')
const controllerBundle = createBundle('./controller.js')
const popupBundle = createBundle('./popup.js')
const zeroBundle = createBundle('./src/mascara.js')
const controllerBundle = createBundle('./src/dapp-connection.js')
const popupBundle = createBundle('./src/popup.js')
const swBuild = createBundle('./src/background.js')
const appBundle = createBundle('./example/index.js')
//
@ -24,6 +26,11 @@ iframeServer.use('/popup', express.static('../dist/chrome'))
iframeServer.get('/controller.js', function(req, res){
res.send(controllerBundle.latest)
})
iframeServer.get('/popup/sw-build.js', function(req, res){
console.log('/sw-build.js')
res.setHeader('Content-Type', 'application/javascript')
res.send(swBuild.latest)
})
// serve background controller
iframeServer.use(express.static('./server'))

139
mascara/src/background.js Normal file
View File

@ -0,0 +1,139 @@
global.window = global
const pipe = require('pump')
const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js')
const connectionListener = new SwGlobalListener(self)
const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex
const PortStream = require('../../app/scripts/lib/port-stream.js')
const DbController = require('./lib/index-db-controller')
const MetamaskController = require('../../app/scripts/metamask-controller')
const extension = {} //require('../../app/scripts/lib/extension')
const storeTransform = require('obs-store/lib/transform')
const Migrator = require('../../app/scripts/lib/migrator/')
const migrations = require('../../app/scripts/migrations/')
const firstTimeState = require('../../app/scripts/first-time-state')
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
let popupIsOpen = false
const log = require('loglevel')
global.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting())
})
self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim())
})
console.log('inside:open')
// // state persistence
let diskStore
const dbController = new DbController({
key: STORAGE_KEY,
version: 2,
})
loadStateFromPersistence()
.then((initState) => setupController(initState))
.then(() => console.log('MetaMask initialization complete.'))
.catch((err) => console.error('WHILE SETTING UP:', err))
// initialization flow
//
// State and Persistence
//
function loadStateFromPersistence() {
// migrations
let migrator = new Migrator({ migrations })
const initialState = migrator.generateInitialState(firstTimeState)
dbController.initialState = initialState
return dbController.open()
.then((versionedData) => migrator.migrateData(versionedData))
.then((versionedData) => {
dbController.put(versionedData)
return Promise.resolve(versionedData)
})
.then((versionedData) => Promise.resolve(versionedData.data))
}
function setupController (initState, client) {
//
// MetaMask Controller
//
const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage: noop,
unlockAccountMessage: noop,
showUnapprovedTx: noop,
// initial state
initState,
})
global.metamaskController = controller
controller.store.subscribe((state) => {
versionifyData(state)
.then((versionedData) => dbController.put(versionedData))
.catch((err) => {console.error(err)})
})
function versionifyData(state) {
return dbController.get()
.then((rawData) => {
return Promise.resolve({
data: state,
meta: rawData.meta,
})}
)
}
//
// connect to other contexts
//
connectionListener.on('remote', (portStream, messageEvent) => {
console.log('REMOTE CONECTION FOUND***********')
connectRemote(portStream, messageEvent.data.context)
})
function connectRemote (connectionStream, context) {
var isMetaMaskInternalProcess = (context === 'popup')
if (isMetaMaskInternalProcess) {
// communication with popup
controller.setupTrustedCommunication(connectionStream, 'MetaMask')
popupIsOpen = true
} else {
// communication with page
setupUntrustedCommunication(connectionStream, context)
}
}
function setupUntrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
controller.setupPublicConfig(mx.createStream('publicConfig'))
}
function setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
}
//
// User Interface setup
//
return Promise.resolve()
}
function noop () {}

View File

@ -0,0 +1,21 @@
const ParentStream = require('iframe-stream').ParentStream
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
const SwStream = require('sw-stream/lib/sw-stream.js')
const SetupUntrustedComunication = ('./lib/setup-untrusted-connection.js')
const background = new SWcontroller({
fileName: '/popup/sw-build.js',
})
const pageStream = new ParentStream()
background.on('ready', (_) => {
let swStream = SwStream({
serviceWorker: background.controller,
context: 'dapp',
})
pageStream.pipe(swStream).pipe(pageStream)
})
background.on('error', console.error)
background.startWorker()

View File

@ -0,0 +1,88 @@
const EventEmitter = require('events')
module.exports = class IndexDbController extends EventEmitter {
constructor (opts) {
super()
this.migrations = opts.migrations
this.key = opts.key
this.dbObject = global.indexedDB
this.IDBTransaction = global.IDBTransaction || global.webkitIDBTransaction || global.msIDBTransaction || {READ_WRITE: "readwrite"}; // This line should only be needed if it is needed to support the object's constants for older browsers
this.IDBKeyRange = global.IDBKeyRange || global.webkitIDBKeyRange || global.msIDBKeyRange;
this.version = opts.version
this.logging = opts.logging
this.initialState = opts.initialState
if (this.logging) this.on('log', logger)
}
// Opens the database connection and returns a promise
open (version = this.version) {
return new Promise((resolve, reject) => {
const dbOpenRequest = this.dbObject.open(this.key, version)
dbOpenRequest.onerror = (event) => {
return reject(event)
}
dbOpenRequest.onsuccess = (event) => {
this.db = dbOpenRequest.result
this.emit('success')
resolve(this.db)
}
dbOpenRequest.onupgradeneeded = (event) => {
this.db = event.target.result
this.db.createObjectStore('dataStore')
}
})
.then((openRequest) => {
return this.get('dataStore')
})
.then((data) => {
if (!data) {
return this._add('dataStore', this.initialState)
.then(() => this.get('dataStore'))
.then((versionedData) => Promise.resolve(versionedData))
}
return Promise.resolve(data)
})
}
requestObjectStore (key, type = 'readonly') {
return new Promise((resolve, reject) => {
const dbReadWrite = this.db.transaction(key, type)
const dataStore = dbReadWrite.objectStore(key)
resolve(dataStore)
})
}
get (key = 'dataStore') {
return this.requestObjectStore(key)
.then((dataObject)=> {
return new Promise((resolve, reject) => {
const getRequest = dataObject.get(key)
getRequest.onsuccess = (event) => resolve(event.currentTarget.result)
getRequest.onerror = (event) => reject(event)
})
})
}
put (state) {
return this.requestObjectStore('dataStore', 'readwrite')
.then((dataObject)=> {
const putRequest = dataObject.put(state, 'dataStore')
putRequest.onsuccess = (event) => Promise.resolve(event.currentTarget.result)
putRequest.onerror = (event) => Promise.reject(event)
})
}
_add (key, objStore, cb = logger) {
return this.requestObjectStore(key, 'readwrite')
.then((dataObject)=> {
const addRequest = dataObject.add(objStore, key)
addRequest.onsuccess = (event) => Promise.resolve(event.currentTarget.result)
addRequest.onerror = (event) => Promise.reject(event)
})
}
}
function logger (err, ress) {
err ? console.error(`Logger says: ${err}`) : console.dir(`Logger says: ${ress}`)
}

View File

@ -1,19 +1,17 @@
const setupIframe = require('./setup-iframe.js')
const MetamaskInpageProvider = require('../../app/scripts/lib/inpage-provider.js')
const MetamaskInpageProvider = require('../../../app/scripts/lib/inpage-provider.js')
module.exports = getProvider
function getProvider(){
if (global.web3) {
console.log('MetaMask ZeroClient - using environmental web3 provider')
return global.web3.currentProvider
}
console.log('MetaMask ZeroClient - injecting zero-client iframe!')
var iframeStream = setupIframe({
zeroClientProvider: 'http://127.0.0.1:9001',
zeroClientProvider: 'http://localhost:9001',
sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'],
container: document.body,
})
@ -22,4 +20,3 @@ function getProvider(){
return inpageProvider
}

View File

@ -10,8 +10,7 @@ var web3 = new Web3(provider)
web3.setProvider = function(){
console.log('MetaMask - overrode web3.setProvider')
}
console.log('metamask lib hijacked provider')
//
//
// export web3
//
@ -26,7 +25,7 @@ var shouldPop = false
window.addEventListener('click', function(){
if (!shouldPop) return
shouldPop = false
window.open('http://127.0.0.1:9001/popup/popup.html', '', 'width=360 height=500')
window.open('http://localhost:9001/popup/popup.html', '', 'width=360 height=500')
console.log('opening window...')
})
@ -41,3 +40,5 @@ function hijackProvider(provider){
_super(payload, cb)
}
}

36
mascara/src/popup.js Normal file
View File

@ -0,0 +1,36 @@
const injectCss = require('inject-css')
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
const SwStream = require('sw-stream/lib/sw-stream.js')
const MetaMaskUiCss = require('../../ui/css')
const setupIframe = require('./lib/setup-iframe.js')
const MetamaskInpageProvider = require('../../app/scripts/lib/inpage-provider.js')
const startPopup = require('../../app/scripts/popup-core')
var css = MetaMaskUiCss()
injectCss(css)
const container = document.getElementById('app-content')
var name = 'popup'
window.METAMASK_UI_TYPE = name
const background = new SWcontroller({
fileName: '/popup/sw-build.js',
})
// Setup listener for when the service worker is read
background.on('ready', (readSw) => {
let connectionStream = SwStream({
serviceWorker: background.controller,
context: name,
})
startPopup({container, connectionStream}, (err, store) => {
if (err) return displayCriticalError(err)
store.subscribe(() => {
const state = store.getState()
if (state.appState.shouldClose) window.close()
})
})
})
background.startWorker()
console.log('hello from /library/popup.js')

View File

@ -43,6 +43,7 @@
"bluebird": "^3.5.0",
"browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4",
"client-sw-ready-event": "^1.0.2",
"clone": "^1.0.2",
"copy-to-clipboard": "^2.0.0",
"debounce": "^1.0.0",
@ -104,6 +105,7 @@
"request-promise": "^4.1.1",
"sandwich-expando": "^1.0.5",
"semaphore": "^1.0.5",
"sw-stream": "^2.0.0",
"textarea-caret": "^3.0.1",
"three.js": "^0.73.2",
"through2": "^2.0.1",

View File

@ -14,7 +14,6 @@ log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn')
function launchMetamaskUi (opts, cb) {
var accountManager = opts.accountManager
actions._setBackgroundConnection(accountManager)
// check if we are unlocked first
accountManager.getState(function (err, metamaskState) {
if (err) return cb(err)