diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js new file mode 100644 index 000000000..94413a1c4 --- /dev/null +++ b/app/scripts/popup-core.js @@ -0,0 +1,63 @@ +const EventEmitter = require('events').EventEmitter +const Dnode = require('dnode') +const Web3 = require('web3') +const MetaMaskUi = require('../../ui') +const StreamProvider = require('web3-stream-provider') +const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex + + +module.exports = initializePopup + + +function initializePopup(connectionStream){ + // setup app + connectToAccountManager(connectionStream, setupApp) +} + +function connectToAccountManager (connectionStream, cb) { + // setup communication with background + // setup multiplexing + var mx = setupMultiplex(connectionStream) + // connect features + setupControllerConnection(mx.createStream('controller'), cb) + setupWeb3Connection(mx.createStream('provider')) +} + +function setupWeb3Connection (connectionStream) { + var providerStream = new StreamProvider() + providerStream.pipe(connectionStream).pipe(providerStream) + connectionStream.on('error', console.error.bind(console)) + providerStream.on('error', console.error.bind(console)) + global.web3 = new Web3(providerStream) +} + +function setupControllerConnection (connectionStream, cb) { + // this is a really sneaky way of adding EventEmitter api + // to a bi-directional dnode instance + var eventEmitter = new EventEmitter() + var accountManagerDnode = Dnode({ + sendUpdate: function (state) { + eventEmitter.emit('update', state) + }, + }) + connectionStream.pipe(accountManagerDnode).pipe(connectionStream) + accountManagerDnode.once('remote', function (accountManager) { + // setup push events + accountManager.on = eventEmitter.on.bind(eventEmitter) + cb(null, accountManager) + }) +} + +function setupApp (err, accountManager) { + if (err) { + alert(err.stack) + throw err + } + + var container = document.getElementById('app-content') + + MetaMaskUi({ + container: container, + accountManager: accountManager, + }) +} diff --git a/app/scripts/popup.js b/app/scripts/popup.js index 096b56115..e6f149f96 100644 --- a/app/scripts/popup.js +++ b/app/scripts/popup.js @@ -1,94 +1,22 @@ -const url = require('url') -const EventEmitter = require('events').EventEmitter -const async = require('async') -const Dnode = require('dnode') -const Web3 = require('web3') -const MetaMaskUi = require('../../ui') -const MetaMaskUiCss = require('../../ui/css') const injectCss = require('inject-css') +const MetaMaskUiCss = require('../../ui/css') +const startPopup = require('./popup-core') const PortStream = require('./lib/port-stream.js') -const StreamProvider = require('web3-stream-provider') -const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const isPopupOrNotification = require('./lib/is-popup-or-notification') const extension = require('./lib/extension') const notification = require('./lib/notifications') -// setup app var css = MetaMaskUiCss() injectCss(css) -async.parallel({ - currentDomain: getCurrentDomain, - accountManager: connectToAccountManager, -}, setupApp) - -function connectToAccountManager (cb) { - // setup communication with background - - var name = isPopupOrNotification() - closePopupIfOpen(name) - window.METAMASK_UI_TYPE = name - var pluginPort = extension.runtime.connect({ name }) - var portStream = new PortStream(pluginPort) - // setup multiplexing - var mx = setupMultiplex(portStream) - // connect features - setupControllerConnection(mx.createStream('controller'), cb) - setupWeb3Connection(mx.createStream('provider')) -} - -function setupWeb3Connection (stream) { - var remoteProvider = new StreamProvider() - remoteProvider.pipe(stream).pipe(remoteProvider) - stream.on('error', console.error.bind(console)) - remoteProvider.on('error', console.error.bind(console)) - global.web3 = new Web3(remoteProvider) -} - -function setupControllerConnection (stream, cb) { - var eventEmitter = new EventEmitter() - var background = Dnode({ - sendUpdate: function (state) { - eventEmitter.emit('update', state) - }, - }) - stream.pipe(background).pipe(stream) - background.once('remote', function (accountManager) { - // setup push events - accountManager.on = eventEmitter.on.bind(eventEmitter) - cb(null, accountManager) - }) -} +var name = isPopupOrNotification() +closePopupIfOpen(name) +window.METAMASK_UI_TYPE = name -function getCurrentDomain (cb) { - const unknown = '' - if (!extension.tabs) return cb(null, unknown) - extension.tabs.query({active: true, currentWindow: true}, function (results) { - var activeTab = results[0] - var currentUrl = activeTab && activeTab.url - var currentDomain = url.parse(currentUrl).host - if (!currentUrl) { - return cb(null, unknown) - } - cb(null, currentDomain) - }) -} +var pluginPort = extension.runtime.connect({ name }) +var portStream = new PortStream(pluginPort) -function setupApp (err, opts) { - if (err) { - alert(err.stack) - throw err - } - - var container = document.getElementById('app-content') - - MetaMaskUi({ - container: container, - accountManager: opts.accountManager, - currentDomain: opts.currentDomain, - networkVersion: opts.networkVersion, - }) -} +startPopup(portStream) function closePopupIfOpen(name) { if (name !== 'notification') { diff --git a/library/README.md b/library/README.md new file mode 100644 index 000000000..7dc291564 --- /dev/null +++ b/library/README.md @@ -0,0 +1,6 @@ +start the dual servers (dapp + mascara) +``` +node server.js +``` + +open the example dapp at `http://localhost:9002/` \ No newline at end of file diff --git a/library/controller.js b/library/controller.js new file mode 100644 index 000000000..d5cd0525e --- /dev/null +++ b/library/controller.js @@ -0,0 +1,68 @@ +const ZeroClientProvider = require('web3-provider-engine/zero') +const ParentStream = require('iframe-stream').ParentStream +const handleRequestsFromStream = require('web3-stream-provider/handler') +const Streams = require('mississippi') +const ObjectMultiplex = require('../app/scripts/lib/obj-multiplex') + + +initializeZeroClient() + +function initializeZeroClient() { + + var provider = ZeroClientProvider({ + // rpcUrl: configManager.getCurrentRpcAddress(), + rpcUrl: 'https://morden.infura.io/', + // account mgmt + // getAccounts: function(cb){ + // var selectedAddress = idStore.getSelectedAddress() + // var result = selectedAddress ? [selectedAddress] : [] + // cb(null, result) + // }, + getAccounts: function(cb){ + cb(null, ['0x8F331A98aC5C9431d04A5d6Bf8Fa84ed7Ed439f3'.toLowerCase()]) + }, + // tx signing + // approveTransaction: addUnconfirmedTx, + // signTransaction: idStore.signTransaction.bind(idStore), + signTransaction: function(txParams, cb){ + var privKey = new Buffer('7ef33e339ba5a5af0e57fa900ad0ae53deaa978c21ef30a0947532135eb639a8', 'hex') + var Transaction = require('ethereumjs-tx') + console.log('signing tx:', txParams) + txParams.gasLimit = txParams.gas + var tx = new Transaction(txParams) + tx.sign(privKey) + var serialiedTx = '0x'+tx.serialize().toString('hex') + cb(null, serialiedTx) + }, + // msg signing + // approveMessage: addUnconfirmedMsg, + // signMessage: idStore.signMessage.bind(idStore), + }) + + provider.on('block', function(block){ + console.log('BLOCK CHANGED:', '#'+block.number.toString('hex'), '0x'+block.hash.toString('hex')) + }) + + var connectionStream = new ParentStream() + // setup connectionStream multiplexing + var multiStream = ObjectMultiplex() + Streams.pipe(connectionStream, multiStream, connectionStream, function(err){ + console.warn('MetamaskIframe - lost connection to Dapp') + if (err) throw err + }) + + // connectionStream.on('data', function(chunk){ console.log('connectionStream chuck', chunk) }) + // multiStream.on('data', function(chunk){ console.log('multiStream chuck', chunk) }) + + var providerStream = multiStream.createStream('provider') + handleRequestsFromStream(providerStream, provider, logger) + + function logger(err, request, response){ + if (err) return console.error(err.stack) + if (!request.isMetamaskInternal) { + console.log('MetaMaskIframe - RPC complete:', request, '->', response) + if (response.error) console.error('Error in RPC response:\n'+response.error.message) + } + } + +} \ No newline at end of file diff --git a/library/example/index.html b/library/example/index.html new file mode 100644 index 000000000..47d6da34f --- /dev/null +++ b/library/example/index.html @@ -0,0 +1,17 @@ + + + + + + + MetaMask ZeroClient Example + + + + + + + + + + \ No newline at end of file diff --git a/library/example/index.js b/library/example/index.js new file mode 100644 index 000000000..a3f4b9859 --- /dev/null +++ b/library/example/index.js @@ -0,0 +1,54 @@ + +window.addEventListener('load', web3Detect) + +function web3Detect() { + if (global.web3) { + logToDom('web3 detected!') + startApp() + } else { + logToDom('no web3 detected!') + } +} + +function startApp(){ + console.log('app started') + + var primaryAccount = null + console.log('getting main account...') + web3.eth.getAccounts(function(err, addresses){ + if (err) throw err + console.log('set address') + primaryAccount = addresses[0] + }) + + document.querySelector('.action-button-1').addEventListener('click', function(){ + console.log('saw click') + console.log('sending tx') + web3.eth.sendTransaction({ + from: primaryAccount, + value: 0, + }, function(err, txHash){ + if (err) throw err + console.log('sendTransaction result:', err || txHash) + }) + }) + document.querySelector('.action-button-2').addEventListener('click', function(){ + console.log('saw click') + setTimeout(function(){ + console.log('sending tx') + web3.eth.sendTransaction({ + from: primaryAccount, + value: 0, + }, function(err, txHash){ + if (err) throw err + console.log('sendTransaction result:', err || txHash) + }) + }) + }) + +} + +function logToDom(message){ + document.body.appendChild(document.createTextNode(message)) + console.log(message) +} \ No newline at end of file diff --git a/library/index.js b/library/index.js new file mode 100644 index 000000000..ded588967 --- /dev/null +++ b/library/index.js @@ -0,0 +1,44 @@ +const Web3 = require('web3') +const setupProvider = require('./lib/setup-provider.js') + +// +// setup web3 +// + +var provider = setupProvider() +hijackProvider(provider) +var web3 = new Web3(provider) +web3.setProvider = function(){ + console.log('MetaMask - overrode web3.setProvider') +} +console.log('metamask lib hijacked provider') + +// +// export web3 +// + +global.web3 = web3 + +// +// ui stuff +// + +var shouldPop = false +window.addEventListener('click', function(){ + if (!shouldPop) return + shouldPop = false + window.open('http://localhost:9001/popup/popup.html', '', 'width=1000') + console.log('opening window...') +}) + + +function hijackProvider(provider){ + var _super = provider.sendAsync.bind(provider) + provider.sendAsync = function(payload, cb){ + if (payload.method === 'eth_sendTransaction') { + console.log('saw send') + shouldPop = true + } + _super(payload, cb) + } +} \ No newline at end of file diff --git a/library/lib/setup-iframe.js b/library/lib/setup-iframe.js new file mode 100644 index 000000000..db67163df --- /dev/null +++ b/library/lib/setup-iframe.js @@ -0,0 +1,19 @@ +const Iframe = require('iframe') +const IframeStream = require('iframe-stream').IframeStream + +module.exports = setupIframe + + +function setupIframe(opts) { + opts = opts || {} + var frame = Iframe({ + src: opts.zeroClientProvider || 'https://zero.metamask.io/', + container: opts.container || document.head, + sandboxAttributes: opts.sandboxAttributes || ['allow-scripts', 'allow-popups'], + }) + var iframe = frame.iframe + iframe.style.setProperty('display', 'none') + var iframeStream = new IframeStream(iframe) + + return iframeStream +} diff --git a/library/lib/setup-provider.js b/library/lib/setup-provider.js new file mode 100644 index 000000000..9efd209cb --- /dev/null +++ b/library/lib/setup-provider.js @@ -0,0 +1,25 @@ +const setupIframe = require('./setup-iframe.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', + sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'], + container: document.body, + }) + + var inpageProvider = new MetamaskInpageProvider(iframeStream) + return inpageProvider + +} + diff --git a/library/popup.js b/library/popup.js new file mode 100644 index 000000000..667b13371 --- /dev/null +++ b/library/popup.js @@ -0,0 +1,19 @@ +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) diff --git a/library/server.js b/library/server.js new file mode 100644 index 000000000..bb0b24e50 --- /dev/null +++ b/library/server.js @@ -0,0 +1,105 @@ +const express = require('express') +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 appBundle = createBundle('./example/index.js') + +// +// Iframe Server +// + +const iframeServer = express() + +// serve popup window +iframeServer.get('/popup/scripts/popup.js', function(req, res){ + res.send(popupBundle.latest) +}) +iframeServer.use('/popup', express.static('../dist/chrome')) + +// serve controller bundle +iframeServer.get('/controller.js', function(req, res){ + res.send(controllerBundle.latest) +}) + +// serve background controller +iframeServer.use(express.static('./server')) + +// start the server +const mascaraPort = 9001 +iframeServer.listen(mascaraPort) +console.log(`Mascara service listening on port ${mascaraPort}`) + + +// +// Dapp Server +// + +const dappServer = express() + +// serve metamask-lib bundle +dappServer.get('/zero.js', function(req, res){ + res.send(zeroBundle.latest) +}) + +// serve dapp bundle +dappServer.get('/app.js', function(req, res){ + res.send(appBundle.latest) +}) + +// serve static +dappServer.use(express.static('./example')) + +// start the server +const dappPort = '9002' +dappServer.listen(dappPort) +console.log(`Dapp listening on port ${dappPort}`) + +// +// util +// + +function serveBundle(entryPoint){ + const bundle = createBundle(entryPoint) + return function(req, res){ + res.send(bundle.latest) + } +} + +function createBundle(entryPoint){ + + var bundleContainer = {} + + var bundler = browserify({ + entries: [entryPoint], + cache: {}, + packageCache: {}, + plugin: [watchify], + }) + + // global transpile + var bablePreset = require.resolve('babel-preset-es2015') + + bundler.transform(babelify, { + global: true, + presets: [bablePreset], + babelrc: false, + }) + + bundler.on('update', bundle) + bundle() + + return bundleContainer + + function bundle() { + bundler.bundle(function(err, result){ + if (err) throw err + console.log(`Bundle updated! (${entryPoint})`) + bundleContainer.latest = result.toString() + }) + } + +} diff --git a/library/server/index.html b/library/server/index.html new file mode 100644 index 000000000..2308dd98b --- /dev/null +++ b/library/server/index.html @@ -0,0 +1,20 @@ + + + + + + + MetaMask ZeroClient Iframe + + + + + + + + Hello! I am the MetaMask iframe. + + + \ No newline at end of file diff --git a/package.json b/package.json index 1fc9a5f7d..c75772a34 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "lint": "gulp lint", "dev": "gulp dev", "dist": "gulp dist", - "test": "npm run fastTest && npm run ci", + "test": "npm run fastTest && npm run ci && npm run lint", "fastTest": "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 ./", @@ -45,9 +45,12 @@ "eth-store": "^1.1.0", "ethereumjs-tx": "^1.0.0", "ethereumjs-util": "^4.4.0", + "express": "^4.14.0", "gulp-eslint": "^2.0.0", "hat": "0.0.3", "identicon.js": "^1.2.1", + "iframe": "^1.0.0", + "iframe-stream": "^1.0.2", "inject-css": "^0.1.1", "jazzicon": "^1.1.3", "menu-droppo": "^1.1.0", @@ -76,7 +79,7 @@ "three.js": "^0.73.2", "through2": "^2.0.1", "vreme": "^3.0.2", - "web3": "^0.17.0-alpha", + "web3": "ethereum/web3.js#260ac6e78a8ce4b2e13f5bb0fdb65f4088585876", "web3-provider-engine": "^8.0.2", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js index d3c84d387..735526c60 100644 --- a/ui/app/accounts/index.js +++ b/ui/app/accounts/index.js @@ -20,7 +20,6 @@ function mapStateToProps (state) { identities: state.metamask.identities, unconfTxs: state.metamask.unconfTxs, selectedAddress: state.metamask.selectedAddress, - currentDomain: state.appState.currentDomain, scrollToBottom: state.appState.scrollToBottom, pending, } diff --git a/ui/app/components/pending-tx-details.js b/ui/app/components/pending-tx-details.js index c2e39a1ca..148b5c6df 100644 --- a/ui/app/components/pending-tx-details.js +++ b/ui/app/components/pending-tx-details.js @@ -1,7 +1,6 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const carratInline = require('fs').readFileSync('./images/forward-carrat.svg', 'utf8') const MiniAccountPanel = require('./mini-account-panel') const EthBalance = require('./eth-balance') @@ -78,7 +77,7 @@ PTXP.render = function () { ]), - forwardCarrat(imageify), + forwardCarrat(), this.miniAccountPanelForRecipient(), ]), @@ -223,30 +222,16 @@ PTXP.warnIfNeeded = function () { } -function forwardCarrat (imageify) { - if (imageify) { - return ( - - h('img', { - src: 'images/forward-carrat.svg', - style: { - padding: '5px 6px 0px 10px', - height: '37px', - }, - }) - - ) - } else { - return ( +function forwardCarrat () { + return ( - h('div', { - dangerouslySetInnerHTML: { __html: carratInline }, - style: { - padding: '0px 6px 0px 10px', - height: '45px', - }, - }) + h('img', { + src: 'images/forward-carrat.svg', + style: { + padding: '5px 6px 0px 10px', + height: '37px', + }, + }) - ) - } + ) } diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index a6cd9ca1b..c39d89a4d 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -39,7 +39,6 @@ function reduceApp (state, action) { accountDetail: { subview: 'transactions', }, - currentDomain: 'example.com', transForward: true, // Used to render transition direction isLoading: false, // Used to display loading indicator warning: null, // Used to display error text diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 7f18480cb..84953d734 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -11,7 +11,6 @@ function reduceMetamask (state, action) { isInitialized: false, isUnlocked: false, isEthConfirmed: false, - currentDomain: 'example.com', rpcTarget: 'https://rawtestrpc.metamask.io/', identities: {}, unconfTxs: {}, diff --git a/ui/index.js b/ui/index.js index 0e69b00d6..a6905b639 100644 --- a/ui/index.js +++ b/ui/index.js @@ -25,9 +25,7 @@ function startApp (metamaskState, accountManager, opts) { metamask: metamaskState, // appState represents the current tab's popup state - appState: { - currentDomain: opts.currentDomain, - }, + appState: {}, // Which blockchain we are using: networkVersion: opts.networkVersion,