https://github.com/MetaMask/mascarajavascript
(beta) Add MetaMask to your dapp even if the user doesn't have the extension installedhtml
能夠開始分析一下這裏的代碼,從java
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node example/server/" },
那麼就從example/server/開始,這裏有兩個文件index.js和util.js:node
index.jsgit
const express = require('express') //const createMetamascaraServer = require('../server/'),這個是本身設置服務器,而不是使用wallet.metamask.io的時候使用的,以後再講 const createBundle = require('./util').createBundle //這兩個的做用其實就是實時監督app.js的變化並將其使用browserify轉成瀏覽器使用的模式app-bundle.js const serveBundle = require('./util').serveBundle // // Dapp Server // const dappServer = express() // serve dapp bundle serveBundle(dappServer, '/app-bundle.js', createBundle(require.resolve('../app.js'))) dappServer.use(express.static(__dirname + '/../app/')) //這樣使用http://localhost:9010訪問時就會去(__dirname + '/../app/')的位置調用index.html // start the server const dappPort = '9010' //網頁監聽端口 dappServer.listen(dappPort) console.log(`Dapp listening on port ${dappPort}`)
util.jsgithub
const browserify = require('browserify') const watchify = require('watchify') module.exports = { serveBundle, createBundle, } function serveBundle(server, path, bundle){//就是當瀏覽器中調用了path時,上面可知爲'/app-bundle.js' server.get(path, function(req, res){ res.setHeader('Content-Type', 'application/javascript; charset=UTF-8') //設置header res.send(bundle.latest) //4 而且返回打包後的文件,便可以用於瀏覽器的app-bundle.js }) } function createBundle(entryPoint){//entryPoint是'../app.js'的完整絕對路徑 var bundleContainer = {} var bundler = browserify({//這一部分的內容與browserify的插件watchify有關 entries: [entryPoint], cache: {}, packageCache: {}, plugin: [watchify],//watchify讓文件每次變更都編譯 }) bundler.on('update', bundle)//2 當文件有變化,就會從新再打包一次,調用bundle() bundle()//1 先執行一次完整的打包 return bundleContainer function bundle() { bundler.bundle(function(err, result){//3 即將browserify後的文件打包成一個 if (err) { console.log(`Bundle failed! (${entryPoint})`) console.error(err) return } console.log(`Bundle updated! (${entryPoint})`) bundleContainer.latest = result.toString()// }) } }
⚠️下面的http://localhost:9001是設置的本地的server port(就是鏈接的區塊鏈的端口),可是從上面的index.js文件能夠看出它這裏只設置了dapp server,端口爲9010,因此這裏咱們不設置host,使用其默認的https://wallet.metamask.io,去調用頁面版web
mascara/example/app/index.htmlexpress
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>MetaMask ZeroClient Example</title> </head> <body> <button id="action-button-1">GET ACCOUNT</button> <div id="account"></div> <button id="action-button-2">SEND TRANSACTION</button> <div id="cb-value" ></div>
<!-- browserify獲得的app-bundle.js就是在這裏使用 --> <script src="./app-bundle.js"></script> <iframe src="https://wallet.metamask.io"></iframe> <!-- <iframe src="http://localhost:9001"></iframe> 將這裏換成了上面的--> </body> </html>
再來就是json
const metamask = require('../mascara') const EthQuery = require('ethjs-query') window.addEventListener('load', loadProvider) window.addEventListener('message', console.warn) // metamask.setupWidget({host: 'http://localhost:9001'}),改了,看下面的lib/setup-widget.js metamask.setupWidget() async function loadProvider() { // const ethereumProvider = metamask.createDefaultProvider({host: 'http://localhost:9001'}),改了 const ethereumProvider = metamask.createDefaultProvider() global.ethQuery = new EthQuery(ethereumProvider) const accounts = await ethQuery.accounts() window.METAMASK_ACCOUNT = accounts[0] || 'locked' logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account') //在<div id="account"></div>處顯示帳戶信息或者'LOCKED or undefined',一開始不點擊get account也會顯示 setupButtons(ethQuery) } function logToDom(message, context){ document.getElementById(context).innerText = message console.log(message) } function setupButtons (ethQuery) { const accountButton = document.getElementById('action-button-1') accountButton.addEventListener('click', async () => {//當點擊了get account按鈕就會顯示你在wallet.metamask.io錢包上的帳戶的信息(當有帳戶且帳戶解鎖)或者'LOCKED or undefined' const accounts = await ethQuery.accounts() window.METAMASK_ACCOUNT = accounts[0] || 'locked' logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account') }) const txButton = document.getElementById('action-button-2') txButton.addEventListener('click', async () => {//當點擊send Transaction按鈕時,將會彈出一個窗口確認交易 if (!window.METAMASK_ACCOUNT || window.METAMASK_ACCOUNT === 'locked') return const txHash = await ethQuery.sendTransaction({//產生一個本身到本身的交易,錢數爲0,但會花費gas from: window.METAMASK_ACCOUNT, to: window.METAMASK_ACCOUNT, data: '', }) logToDom(txHash, 'cb-value')//而後在<div id="cb-value" ></div>處獲得交易hash }) }
接下來就是const metamask = require('../mascara')中調用的
mascara/mascara.js
const setupProvider = require('./lib/setup-provider.js') const setupDappAutoReload = require('./lib/auto-reload.js') const setupWidget = require('./lib/setup-widget.js') const config = require('./config.json')//設置了調用後會致使彈出窗口的方法 module.exports = { createDefaultProvider, // disabled for now setupWidget, } function createDefaultProvider (opts = {}) {//1使用這個來設置你鏈接的本地區塊鏈等,若是沒有設置則默認爲鏈接一個在線版的metamask錢包 const host = opts.host || 'https://wallet.metamask.io' //2 這裏host假設設置index.js處寫的http://localhost:9001,那麼就會調用本地,而不會去調用線上錢包了https://wallet.metamask.io // // setup provider // const provider = setupProvider({//3這個就會去調用setup-provider.js中的getProvider(opts)函數,opts爲{mascaraUrl: 'http://localhost:9001/proxy/'},或'http://wallet.metamask.io/proxy/' mascaraUrl: host + '/proxy/', })//14 而後這裏就可以獲得inpagePrivider instrumentForUserInteractionTriggers(provider)//15 就是若是用戶經過provider.sendAsync異步調用的是config.json中指明的幾個運行要彈出頁面的方法的話 // // ui stuff // let shouldPop = false//17若是用戶調用的不是須要彈窗的方法,則設置爲false window.addEventListener('click', maybeTriggerPopup)//18 當頁面有點擊的操做時,調用函數maybeTriggerPopup return !window.web3 ? setupDappAutoReload(provider, provider.publicConfigStore) : provider // // util // function maybeTriggerPopup(event){//19 查看是否須要彈出窗口 if (!shouldPop) return//20 不須要則返回 shouldPop = false//21須要則先設爲false window.open(host, '', 'width=360 height=500')//22 而後打開一個窗口,host爲你設置的區塊鏈http://localhost:9001,或者在線錢包'https://wallet.metamask.io'設置的彈出頁面 } function instrumentForUserInteractionTriggers(provider){//用來查看調用的方法是否須要彈出窗口,若是須要就將shouldPop設爲true if (window.web3) return provider const _super = provider.sendAsync.bind(provider)//16 將_super上下文環境設置爲傳入的provider環境 provider.sendAsync = function (payload, cb) {//16 從新定義provider.sendAsync要先設置shouldPop = true if (config.ethereum['should-show-ui'].includes(payload.method)) { shouldPop = true } _super(payload, cb)//16 而後再次調用該_super方法,即在傳入的provider環境運行provider.sendAsync函數,就是使用的仍是以前的provider.sendAsync方法,而不是上面新定義的方法 } } } // function setupWidget (opts = {}) { // }
接下來就是對lib文檔的講解了
const setupIframe = require('./setup-iframe.js') const MetamaskInpageProvider = require('./inpage-provider.js') module.exports = getProvider function getProvider(opts){//4 opts爲{mascaraUrl: 'http://localhost:9001/proxy/'}或'http://wallet.metamask.io/proxy/' if (global.web3) {//5 若是測試到全局有一個web3接口,就說明鏈接的是在線錢包,那麼就返回在線錢包的provider console.log('MetaMask ZeroClient - using environmental web3 provider') return global.web3.currentProvider } console.log('MetaMask ZeroClient - injecting zero-client iframe!') let iframeStream = setupIframe({//6 不然就說明咱們使用的是本身的區塊鏈,那麼就要插入mascara iframe了,調用setup-iframe.js的setupIframe(opts) zeroClientProvider: opts.mascaraUrl,//7 opts = {zeroClientProvider: 'http://localhost:9001/proxy/'}或'http://wallet.metamask.io/proxy/' })//返回Iframe{src:'http://localhost:9001/proxy/',container:document.head,sandboxAttributes:['allow-scripts', 'allow-popups', 'allow-same-origin']} return new MetamaskInpageProvider(iframeStream)//11 13 MetamaskInpageProvider與頁面鏈接,返回其self做爲provider }
const Iframe = require('iframe')//看本博客的iframe-metamask學習使用 const createIframeStream = require('iframe-stream').IframeStream function setupIframe(opts) {//8 opts = {zeroClientProvider: 'http://localhost:9001/proxy/'}或'http://wallet.metamask.io/proxy/' opts = opts || {} let frame = Iframe({//9 設置<Iframe>內容屬性 src: opts.zeroClientProvider || 'https://wallet.metamask.io/', container: opts.container || document.head, sandboxAttributes: opts.sandboxAttributes || ['allow-scripts', 'allow-popups', 'allow-same-origin'], }) let iframe = frame.iframe iframe.style.setProperty('display', 'none')//至關於style="display:none,將其設置爲隱藏 return createIframeStream(iframe)//10建立一個IframeStream流並返回,Iframe{src:'http://localhost:9001/proxy/',container:document.head,sandboxAttributes:['allow-scripts', 'allow-popups', 'allow-same-origin']} } module.exports = setupIframe
sandbox是安全級別,加上sandbox表示該iframe框架的限制:
值 | 描述 |
---|---|
"" | 應用如下全部的限制。 |
allow-same-origin | 容許 iframe 內容與包含文檔是有相同的來源的 |
allow-top-navigation | 容許 iframe 內容是從包含文檔導航(加載)內容。 |
allow-forms | 容許表單提交。 |
allow-scripts | 容許腳本執行。 |
mascara/lib/inpage-provider.js 詳細學習看本博客MetaMask/metamask-inpage-provider
const pump = require('pump') const RpcEngine = require('json-rpc-engine') const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware') const createStreamMiddleware = require('json-rpc-middleware-stream') const LocalStorageStore = require('obs-store') const ObjectMultiplex = require('obj-multiplex') const config = require('../config.json') module.exports = MetamaskInpageProvider function MetamaskInpageProvider (connectionStream) {//12 connectionStream爲生成的IframeStream const self = this // setup connectionStream multiplexing const mux = self.mux = new ObjectMultiplex() pump( connectionStream, mux, connectionStream, (err) => logStreamDisconnectWarning('MetaMask', err) ) // subscribe to metamask public config (one-way) self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) pump( mux.createStream('publicConfig'), self.publicConfigStore, (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) ) // ignore phishing warning message (handled elsewhere) mux.ignoreStream('phishing') // connect to async provider const streamMiddleware = createStreamMiddleware() pump( streamMiddleware.stream, mux.createStream('provider'), streamMiddleware.stream, (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) ) // handle sendAsync requests via dapp-side rpc engine const rpcEngine = new RpcEngine() rpcEngine.push(createIdRemapMiddleware()) // deprecations rpcEngine.push((req, res, next, end) =>{ const deprecationMessage = config['ethereum']['deprecated-methods'][req.method]//看你是否是用了eth_sign這個將要被棄用的方法 if (!deprecationMessage) return next()//若是不是的話,就繼續往下執行 end(new Error(`MetaMask - ${deprecationMessage}`))//若是是的話,就返回棄用的消息,並推薦使用新方法eth_signTypedData }) rpcEngine.push(streamMiddleware) self.rpcEngine = rpcEngine } // handle sendAsync requests via asyncProvider // also remap ids inbound and outbound MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) { const self = this self.rpcEngine.handle(payload, cb) } MetamaskInpageProvider.prototype.send = function (payload) { const self = this let selectedAddress let result = null switch (payload.method) { case 'eth_accounts': // read from localStorage selectedAddress = self.publicConfigStore.getState().selectedAddress result = selectedAddress ? [selectedAddress] : [] break case 'eth_coinbase': // read from localStorage selectedAddress = self.publicConfigStore.getState().selectedAddress result = selectedAddress || null break case 'eth_uninstallFilter': self.sendAsync(payload, noop) result = true break case 'net_version': const networkVersion = self.publicConfigStore.getState().networkVersion result = networkVersion || null break // throw not-supported Error default: let link = 'https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#dizzy-all-async---think-of-metamask-as-a-light-client' let message = `The MetaMask Web3 object does not support synchronous methods like ${payload.method} without a callback parameter. See ${link} for details.` throw new Error(message) } // return the result return { id: payload.id, jsonrpc: payload.jsonrpc, result: result, } } MetamaskInpageProvider.prototype.isConnected = function () { return true } MetamaskInpageProvider.prototype.isMetaMask = true // util function logStreamDisconnectWarning (remoteLabel, err) { let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` if (err) warningMsg += '\n' + err.stack console.warn(warningMsg) } function noop () {}
const Iframe = require('iframe') module.exports = function setupWidget (opts = {}) { let iframe let style = ` border: 0px; position: absolute; right: 0; top: 0; height: 7rem;` let resizeTimeout const changeStyle = () => { iframe.style = style + (window.outerWidth > 575 ? 'width: 19rem;' : 'width: 7rem;') } const resizeThrottler = () => { if ( !resizeTimeout ) { resizeTimeout = setTimeout(() => { resizeTimeout = null; changeStyle(); // 15fps }, 66); } } window.addEventListener('load', () => { if (window.web3) return const frame = Iframe({ src: `${opts.host}/proxy/widget.html` || 'https://wallet.metamask.io/proxy/widget.html',//下面被改掉了 container: opts.container || document.body, sandboxAttributes: opts.sandboxAttributes || ['allow-scripts', 'allow-popups', 'allow-same-origin', 'allow-top-navigation'], scrollingDisabled: true, }) iframe = frame.iframe changeStyle() }) window.addEventListener('resize', resizeThrottler, false); }
mascara/config.json
說明哪些方法是要彈出窗口來讓用戶confirm的
{
"ethereum": { "deprecated-methods": { "eth_sign": "eth_sign has been deprecated in metamascara due to security concerns please use eth_signTypedData" }, "should-show-ui": [//會致使窗口彈出的method "eth_personalSign", "eth_signTypedData", "eth_sendTransaction" ] } }
而後咱們在終端運行node example/server/來打開dapp server,而後在瀏覽器中運行http://localhost:9010來訪問:
由於我以前有在Chrome瀏覽器中訪問過線上錢包,因此這個時候它可以get account 獲得我在線上錢包的帳戶
點擊send Transaction後,就可以獲得彈窗信息了:
從上面咱們能夠看見有出現很對的錯誤信息,那個主要是由於想要在<iframe></iframe>中顯示線上錢包的內容致使的,可是咱們能夠看見,線上錢包拒絕了這樣的訪問
在上面咱們能夠看見有一個錯誤信息cannot get /undefined/proxy/index.html,解決方法是將lib/setup-widget.js中下面的代碼改了:
// src: `${opts.host}/proxy/index.html` || 'https://wallet.metamask.io/proxy/index.html',改爲: src: 'https://wallet.metamask.io/proxy/index.html',
改後:
改爲:
src: 'https://wallet.metamask.io/proxy/widget.html',
發現widget.html 這個file好像是不存在的,算是這個的bug吧
點擊comfirm後,就會獲得交易hash值:
0x4d1ff956c4fdaafc7cb0a2ca3e144a0bf7534e6db70d3caade2b2ebdfd4f6c20
而後咱們能夠去etherscan中查看這筆交易是否成功,發現是成功了的: