1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-23 03:36:18 +02:00

Merge pull request #651 from MetaMask/library

MetaMask "Mascara" Library - initial PR
This commit is contained in:
kumavis 2016-09-13 11:29:46 -07:00 committed by GitHub
commit b13eaaa0cd
18 changed files with 465 additions and 114 deletions

63
app/scripts/popup-core.js Normal file
View File

@ -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,
})
}

View File

@ -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 injectCss = require('inject-css')
const MetaMaskUiCss = require('../../ui/css')
const startPopup = require('./popup-core')
const PortStream = require('./lib/port-stream.js') 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 isPopupOrNotification = require('./lib/is-popup-or-notification')
const extension = require('./lib/extension') const extension = require('./lib/extension')
const notification = require('./lib/notifications') const notification = require('./lib/notifications')
// setup app
var css = MetaMaskUiCss() var css = MetaMaskUiCss()
injectCss(css) injectCss(css)
async.parallel({ var name = isPopupOrNotification()
currentDomain: getCurrentDomain, closePopupIfOpen(name)
accountManager: connectToAccountManager, window.METAMASK_UI_TYPE = name
}, setupApp)
function connectToAccountManager (cb) { var pluginPort = extension.runtime.connect({ name })
// setup communication with background var portStream = new PortStream(pluginPort)
var name = isPopupOrNotification() startPopup(portStream)
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)
})
}
function getCurrentDomain (cb) {
const unknown = '<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)
})
}
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,
})
}
function closePopupIfOpen(name) { function closePopupIfOpen(name) {
if (name !== 'notification') { if (name !== 'notification') {

6
library/README.md Normal file
View File

@ -0,0 +1,6 @@
start the dual servers (dapp + mascara)
```
node server.js
```
open the example dapp at `http://localhost:9002/`

68
library/controller.js Normal file
View File

@ -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)
}
}
}

View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MetaMask ZeroClient Example</title>
</head>
<body>
<button class="action-button-1">SYNC TX</button>
<button class="action-button-2">ASYNC TX</button>
<script src="./zero.js"></script>
<script src="./app.js"></script>
</body>
</html>

54
library/example/index.js Normal file
View File

@ -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)
}

44
library/index.js Normal file
View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

19
library/popup.js Normal file
View File

@ -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)

105
library/server.js Normal file
View File

@ -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()
})
}
}

20
library/server/index.html Normal file
View File

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MetaMask ZeroClient Iframe</title>
<meta name="description" content="MetaMask ZeroClient">
<meta name="author" content="MetaMask">
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
Hello! I am the MetaMask iframe.
<script src="/controller.js"></script>
</body>
</html>

View File

@ -8,7 +8,7 @@
"lint": "gulp lint", "lint": "gulp lint",
"dev": "gulp dev", "dev": "gulp dev",
"dist": "gulp dist", "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\"", "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\"", "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 ./", "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", "eth-store": "^1.1.0",
"ethereumjs-tx": "^1.0.0", "ethereumjs-tx": "^1.0.0",
"ethereumjs-util": "^4.4.0", "ethereumjs-util": "^4.4.0",
"express": "^4.14.0",
"gulp-eslint": "^2.0.0", "gulp-eslint": "^2.0.0",
"hat": "0.0.3", "hat": "0.0.3",
"identicon.js": "^1.2.1", "identicon.js": "^1.2.1",
"iframe": "^1.0.0",
"iframe-stream": "^1.0.2",
"inject-css": "^0.1.1", "inject-css": "^0.1.1",
"jazzicon": "^1.1.3", "jazzicon": "^1.1.3",
"menu-droppo": "^1.1.0", "menu-droppo": "^1.1.0",
@ -76,7 +79,7 @@
"three.js": "^0.73.2", "three.js": "^0.73.2",
"through2": "^2.0.1", "through2": "^2.0.1",
"vreme": "^3.0.2", "vreme": "^3.0.2",
"web3": "^0.17.0-alpha", "web3": "ethereum/web3.js#260ac6e78a8ce4b2e13f5bb0fdb65f4088585876",
"web3-provider-engine": "^8.0.2", "web3-provider-engine": "^8.0.2",
"web3-stream-provider": "^2.0.6", "web3-stream-provider": "^2.0.6",
"xtend": "^4.0.1" "xtend": "^4.0.1"

View File

@ -20,7 +20,6 @@ function mapStateToProps (state) {
identities: state.metamask.identities, identities: state.metamask.identities,
unconfTxs: state.metamask.unconfTxs, unconfTxs: state.metamask.unconfTxs,
selectedAddress: state.metamask.selectedAddress, selectedAddress: state.metamask.selectedAddress,
currentDomain: state.appState.currentDomain,
scrollToBottom: state.appState.scrollToBottom, scrollToBottom: state.appState.scrollToBottom,
pending, pending,
} }

View File

@ -1,7 +1,6 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const carratInline = require('fs').readFileSync('./images/forward-carrat.svg', 'utf8')
const MiniAccountPanel = require('./mini-account-panel') const MiniAccountPanel = require('./mini-account-panel')
const EthBalance = require('./eth-balance') const EthBalance = require('./eth-balance')
@ -78,7 +77,7 @@ PTXP.render = function () {
]), ]),
forwardCarrat(imageify), forwardCarrat(),
this.miniAccountPanelForRecipient(), this.miniAccountPanelForRecipient(),
]), ]),
@ -223,30 +222,16 @@ PTXP.warnIfNeeded = function () {
} }
function forwardCarrat (imageify) { function forwardCarrat () {
if (imageify) { return (
return (
h('img', { h('img', {
src: 'images/forward-carrat.svg', src: 'images/forward-carrat.svg',
style: { style: {
padding: '5px 6px 0px 10px', padding: '5px 6px 0px 10px',
height: '37px', height: '37px',
}, },
}) })
) )
} else {
return (
h('div', {
dangerouslySetInnerHTML: { __html: carratInline },
style: {
padding: '0px 6px 0px 10px',
height: '45px',
},
})
)
}
} }

View File

@ -39,7 +39,6 @@ function reduceApp (state, action) {
accountDetail: { accountDetail: {
subview: 'transactions', subview: 'transactions',
}, },
currentDomain: 'example.com',
transForward: true, // Used to render transition direction transForward: true, // Used to render transition direction
isLoading: false, // Used to display loading indicator isLoading: false, // Used to display loading indicator
warning: null, // Used to display error text warning: null, // Used to display error text

View File

@ -11,7 +11,6 @@ function reduceMetamask (state, action) {
isInitialized: false, isInitialized: false,
isUnlocked: false, isUnlocked: false,
isEthConfirmed: false, isEthConfirmed: false,
currentDomain: 'example.com',
rpcTarget: 'https://rawtestrpc.metamask.io/', rpcTarget: 'https://rawtestrpc.metamask.io/',
identities: {}, identities: {},
unconfTxs: {}, unconfTxs: {},

View File

@ -25,9 +25,7 @@ function startApp (metamaskState, accountManager, opts) {
metamask: metamaskState, metamask: metamaskState,
// appState represents the current tab's popup state // appState represents the current tab's popup state
appState: { appState: {},
currentDomain: opts.currentDomain,
},
// Which blockchain we are using: // Which blockchain we are using:
networkVersion: opts.networkVersion, networkVersion: opts.networkVersion,