The most confusing part about porting MetaMask to a new platform is the way we provide the Web3 API over a series of streams between contexts. Once you understand how we create the InpageProvider in the inpage.js script, you will be able to understand how the port-stream is just a thin wrapper around the postMessage API, and a similar stream API can be wrapped around any communication channel to communicate with the MetaMaskController
via its setupUntrustedCommunication(stream, domain)
method.javascript
將MetaMask移植到新平臺上最使人困惑的部分是咱們經過上下文之間的一系列流提供Web3 API的方式。當你瞭解了咱們如何在inpage.js腳本中建立InpageProvider以後,您將可以理解端口流爲什麼只是一個圍繞postMessage API的thin包裝器以及爲什麼一個相似的流API能夠被包裝在任何通訊通道上,經過它的setupUntrustedCommunication(流,域)方法與MetaMaskController進行通訊。html
postMessage API:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessagejava
window.postMessage() 方法能夠安全地實現跨源通訊。一般,對於兩個不一樣頁面的腳本,只有當執行它們的頁面位於具備相同的協議(一般爲https),端口號(443爲https的默認值),以及主機 (兩個頁面的模數 Document.domain
設置爲相同的值) 時,這兩個腳本才能相互通訊。window.postMessage() 方法提供了一種受控機制來規避此限制,只要正確的使用,這種方法就很安全。git
在本地測試一下,經過下面UI code,調用web3:github
<script type="text/javascript"> //這些註釋的地方都是以前 window.addEventListener('load', function() { console.log(window.web3); //調用web3 }); </script>
獲得結果:web
metamask-extension/app/scripts/inpage.jsjson
/*global Web3*/ cleanContextForImports() require('web3/dist/web3.min.js') const log = require('loglevel')//在本博客loglevel-metamask有介紹 const LocalMessageDuplexStream = require('post-message-stream')//本博客post-message-stream的學習-metamask const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('./lib/inpage-provider.js') restoreContextAfterImports() log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') // // setup plugin communication // // setup background connection var metamaskStream = new LocalMessageDuplexStream({//爲頁面inpage與contentscript創建雙向流鏈接 name: 'inpage', //如上面的例子所示 target: 'contentscript', }) // compose the inpage provider ,而後組成inpageProvider var inpageProvider = new MetamaskInpageProvider(metamaskStream) // // setup web3 // if (typeof window.web3 !== 'undefined') { //查看頁面是否鏈接上了除了metamask之外的web3,有則刪除;由於此時metamask尚未構建好本身自己的web3 throw new Error(`MetaMask detected another web3. MetaMask will not work reliably with another web3 extension. This usually happens if you have two MetaMasks installed, or MetaMask and another web3 extension. Please remove one and try again.`) } var web3 = new Web3(inpageProvider) //而後將上面構建的inpageProvider部署到其自身的web3中 web3.setProvider = function () { log.debug('MetaMask - overrode web3.setProvider') } log.debug('MetaMask - injected web3')//到這裏metamask的injected web3就部署好了 setupDappAutoReload(web3, inpageProvider.publicConfigStore) //這樣但dapp安裝metamask就可以直接使用web3了,由於它會自動下載好 //在上面例子的測試結果中也能看見publicConfigStore的信息 // export global web3, with usage-detection and deprecation warning /* TODO: Uncomment this area once auto-reload.js has been deprecated: let hasBeenWarned = false global.web3 = new Proxy(web3, { get: (_web3, key) => { // show warning once on web3 access if (!hasBeenWarned && key !== 'currentProvider') { console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation') hasBeenWarned = true } // return value normally return _web3[key] }, set: (_web3, key, value) => { // set value normally _web3[key] = value }, }) */ // set web3 defaultAccount inpageProvider.publicConfigStore.subscribe(function (state) {//經過subscribe獲得整個publicConfigStore存儲的state信息,而後再從中獲得selectedAddress web3.eth.defaultAccount = state.selectedAddress }) // need to make sure we aren't affected by overlapping namespaces // and that we dont affect the app with our namespace // mostly a fix for web3's BigNumber if AMD's "define" is defined... var __define /** * Caches reference to global define object and deletes it to * avoid conflicts with other global define objects, such as * AMD's define function */ function cleanContextForImports () { __define = global.define try { global.define = undefined } catch (_) { console.warn('MetaMask - global.define could not be deleted.') } } /** * Restores global define object from cached reference */ function restoreContextAfterImports () { try { global.define = __define } catch (_) { console.warn('MetaMask - global.define could not be overwritten.') } }
上面代碼調用的一些其餘代碼的解釋:gulp
pump = require('pump')
https://github.com/terinjokes/gulp-uglify/blob/master/docs/why-use-pump/README.md#why-use-pump安全
當使用來自Node.js的管道時,錯誤不會經過管道流向前傳播,若是目標流關閉,源流也不會關閉。pump模塊將這些問題規範化,並在回調中傳遞錯誤。app
pump可使咱們更容易找到代碼出錯位置。
更詳細的內容看被博客的 pump模塊的學習-metamask
metamask-inpage-provider/index.js
https://github.com/MetaMask/metamask-inpage-provider/blob/master/index.js
const pump = require('pump') const RpcEngine = require('json-rpc-engine') //ethereum的方法是經過json-rpc進行調用的,這就是a tool for processing JSON RPC,看下面 const createErrorMiddleware = require('./createErrorMiddleware') const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware') //設置了相應的push,看下面 const createStreamMiddleware = require('json-rpc-middleware-stream') const LocalStorageStore = require('obs-store') const asStream = require('obs-store/lib/asStream') //創建有關obs-store的雙向流 const ObjectMultiplex = require('obj-multiplex') const util = require('util') const EventEmitter = require('events') module.exports = MetamaskInpageProvider util.inherits(MetamaskInpageProvider, EventEmitter) function MetamaskInpageProvider (connectionStream) { const self = this // setup connectionStream multiplexing 創建多路複用的鏈接流 const mux = self.mux = new ObjectMultiplex() pump( connectionStream, mux, connectionStream, logStreamDisconnectWarning.bind(this, 'MetaMask') ) // subscribe to metamask public config (one-way) self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) pump( mux.createStream('publicConfig'), asStream(self.publicConfigStore), logStreamDisconnectWarning.bind(this, 'MetaMask PublicConfigStore') ) // ignore phishing warning message (handled elsewhere) mux.ignoreStream('phishing') //忽略釣魚網站 // connect to async provider const streamMiddleware = createStreamMiddleware() pump( streamMiddleware.stream, mux.createStream('provider'), streamMiddleware.stream, logStreamDisconnectWarning.bind(this, 'MetaMask RpcProvider') ) // handle sendAsync requests via dapp-side rpc engine const rpcEngine = new RpcEngine() rpcEngine.push(createIdRemapMiddleware()) //做用看下面 rpcEngine.push(createErrorMiddleware())//用於獲得操做的錯誤並顯示相應信息 rpcEngine.push(streamMiddleware) self.rpcEngine = rpcEngine } // handle sendAsync requests via asyncProvider // also remap ids inbound and outbound MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {//若是網頁上調用web3使用的是那些須要異步等待返回結果的方法的時候其實就是來這裏調用MetamaskInpageProvider.prototype.sendAsync這個方法 const self = this if (payload.method === 'eth_signTypedData') {//這個方法在下一個版本就過期了,不用了 console.warn('MetaMask: This experimental version of eth_signTypedData will be deprecated in the next release in favor of the standard as defined in EIP-712. See https://git.io/fNzPl for more information on the new standard.') } self.rpcEngine.handle(payload, cb) } MetamaskInpageProvider.prototype.send = function (payload) {//若是網頁上調用這些不用異步就可以直接獲得結果的方法的時候,其實就是調用了MetamaskInpageProvider.prototype.send這個函數 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: var link = 'https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#dizzy-all-async---think-of-metamask-as-a-light-client' var 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) const listeners = this.listenerCount('error') if (listeners > 0) { this.emit('error', warningMsg) } } function noop () {}
https://github.com/MetaMask/json-rpc-engine
a tool for processing JSON RPC
const RpcEngine = require('json-rpc-engine') let engine = new RpcEngine()
Build a stack of json rpc processors by pushing in RpcEngine middleware.經過push RpcEngine中間件構建一個json rpc處理器堆棧,處理步驟爲先進後出,handle時獲得的結果是與push時做出的處理相關的
engine.push(function(req, res, next, end){ res.result = 42 end() })
JSON RPC are handled asynchronously, stepping down the stack until complete.異步處理request,直到返回結果
let request = { id: 1, jsonrpc: '2.0', method: 'hello' } engine.handle(request, function(err, res){ // do something with res.result,res.result即爲push中設置的true })
RpcEngine middleware has direct access to the request and response objects. It can let processing continue down the stack with next()
or complete the request with end()
.RpcEngine中間件能夠直接訪問請求和響應對象。它可使用next()繼續處理堆棧,也可使用end()完成請求
engine.push(function(req, res, next, end){ if (req.skipCache) return next() res.result = getResultFromCache(req) end() })
By passing a 'return handler' to the next
function, you can get a peek at the result before it returns.經過將「返回處理程序」傳遞給下一個函數,您能夠在結果返回以前看到它
engine.push(function(req, res, next, end){
next(function(cb){//就是先壓入堆棧中,不進行處理,等到因此push都解決完後再返回處理
insertIntoCache(res, cb)
})
})
RpcEngines can be nested by converting them to middleware asMiddleware(engine)。rpcengine能夠經過將它們轉換爲中間件(中間件)來嵌套
const asMiddleware = require('json-rpc-engine/lib/asMiddleware') let engine = new RpcEngine() let subengine = new RpcEngine() engine.push(asMiddleware(subengine))
Handle errors via end(err)
, NOT next(err)
.解決error使用的是end(),而不是next()
/* INCORRECT */ engine.push(function(req, res, next, end){ next(new Error()) }) /* CORRECT */ engine.push(function(req, res, next, end){ end(new Error()) })
json-rpc-engine/test/basic.spec.js
舉例說明:
/* eslint-env mocha */ 'use strict' const assert = require('assert') const RpcEngine = require('../src/index.js') describe('basic tests', function () { it('basic middleware test', function (done) { let engine = new RpcEngine() engine.push(function (req, res, next, end) { req.method = 'banana' res.result = 42 end() }) let payload = { id: 1, jsonrpc: '2.0', method: 'hello' } engine.handle(payload, function (err, res) { assert.ifError(err, 'did not error') assert(res, 'has res') assert.equal(res.result, 42, 'has expected result') assert.equal(payload.method, 'hello', 'original request object is not mutated by middleware') //payload.method仍然是'hello',而不會被改爲'banana' done() }) }) it('interacting middleware test', function (done) { //兩個push交互 let engine = new RpcEngine() engine.push(function (req, res, next, end) { req.resultShouldBe = 42 next() }) engine.push(function (req, res, next, end) { res.result = req.resultShouldBe end() }) let payload = { id: 1, jsonrpc: '2.0', method: 'hello' } engine.handle(payload, function (err, res) { assert.ifError(err, 'did not error') assert(res, 'has res') assert.equal(res.result, 42, 'has expected result') done() }) }) it('erroring middleware test', function (done) { let engine = new RpcEngine() engine.push(function (req, res, next, end) { end(new Error('no bueno')) }) let payload = { id: 1, jsonrpc: '2.0', method: 'hello' } engine.handle(payload, function (err, res) { assert(err, 'did error') assert(res, 'does have response') assert(res.error, 'does have error on response') done() }) }) it('empty middleware test', function (done) { let engine = new RpcEngine() let payload = { id: 1, jsonrpc: '2.0', method: 'hello' } //若是沒有push。handle將報錯 engine.handle(payload, function (err, res) { assert(err, 'did error') done() }) }) it('handle batch payloads', function (done) { let engine = new RpcEngine() engine.push(function (req, res, next, end) { res.result = req.id end() }) let payloadA = { id: 1, jsonrpc: '2.0', method: 'hello' } let payloadB = { id: 2, jsonrpc: '2.0', method: 'hello' } let payload = [payloadA, payloadB] //能夠一會兒handle多個push engine.handle(payload, function (err, res) { assert.ifError(err, 'did not error') assert(res, 'has res') assert(Array.isArray(res), 'res is array') assert.equal(res[0].result, 1, 'has expected result') assert.equal(res[1].result, 2, 'has expected result') done() }) }) it('return handlers test', function (done) { let engine = new RpcEngine() engine.push(function (req, res, next, end) { next(function (cb) { res.sawReturnHandler = true cb() }) }) engine.push(function (req, res, next, end) { res.result = true end() }) let payload = { id: 1, jsonrpc: '2.0', method: 'hello' } engine.handle(payload, function (err, res) { assert.ifError(err, 'did not error') assert(res, 'has res') assert(res.sawReturnHandler, 'saw return handler') done() }) }) it('return order of events', function (done) { let engine = new RpcEngine() let events = [] engine.push(function (req, res, next, end) { events.push('1-next') next(function (cb) { events.push('1-return') cb() }) }) engine.push(function (req, res, next, end) { events.push('2-next') next(function (cb) { events.push('2-return') cb() }) }) engine.push(function (req, res, next, end) { events.push('3-end') res.result = true end() }) let payload = { id: 1, jsonrpc: '2.0', method: 'hello' } engine.handle(payload, function (err, res) { assert.ifError(err, 'did not error') //說明next只是將處理程序先壓入堆棧中,結果返回前再按先進後出的順序處理 assert.equal(events[0], '1-next', '(event 0) was "1-next"') assert.equal(events[1], '2-next', '(event 1) was "2-next"') assert.equal(events[2], '3-end', '(event 2) was "3-end"') assert.equal(events[3], '2-return', '(event 3) was "2-return"') assert.equal(events[4], '1-return', '(event 4) was "1-return"') done() }) }) })
json-rpc-engine/test/asMiddleware.spec.js
/* eslint-env mocha */ 'use strict' const assert = require('assert') const RpcEngine = require('../src/index.js') const asMiddleware = require('../src/asMiddleware.js') describe('asMiddleware', function () { //嵌套 it('basic', function (done) { let engine = new RpcEngine() let subengine = new RpcEngine() let originalReq subengine.push(function (req, res, next, end) { originalReq = req res.result = 'saw subengine' end() }) engine.push(asMiddleware(subengine)) let payload = { id: 1, jsonrpc: '2.0', method: 'hello' } engine.handle(payload, function (err, res) { assert.ifError(err, 'did not error') assert(res, 'has res') assert.equal(originalReq.id, res.id, 'id matches') assert.equal(originalReq.jsonrpc, res.jsonrpc, 'jsonrpc version matches') assert.equal(res.result, 'saw subengine', 'response was handled by nested engine') done() }) }) })
json-rpc-engine/src/idRemapMiddleware.js
const getUniqueId = require('./getUniqueId') module.exports = createIdRemapMiddleware function createIdRemapMiddleware() { return (req, res, next, end) => { const originalId = req.id const newId = getUniqueId() req.id = newId res.id = newId next((done) => { req.id = originalId res.id = originalId done() }) } }
測試:
json-rpc-engine/test/idRemapMiddleware.spec.js
/* eslint-env mocha */ 'use strict' const assert = require('assert') const RpcEngine = require('../src/index.js') const createIdRemapMiddleware = require('../src/idRemapMiddleware.js') describe('idRemapMiddleware tests', function () { it('basic middleware test', function (done) { let engine = new RpcEngine() const observedIds = { before: {}, after: {}, } engine.push(function (req, res, next, end) { observedIds.before.req = req.id observedIds.before.res = res.id //設置使得handle時 res.id = req.id,二者結果相同 next() }) engine.push(createIdRemapMiddleware()) engine.push(function (req, res, next, end) { observedIds.after.req = req.id observedIds.after.res = res.id // set result so it doesnt error res.result = true end() }) let payload = { id: 1, jsonrpc: '2.0', method: 'hello' } const payloadCopy = Object.assign({}, payload) engine.handle(payload, function (err, res) { assert.ifError(err, 'did not error') assert(res, 'has res') // collected data assert(observedIds.before.req, 'captured ids') assert(observedIds.before.res, 'captured ids') assert(observedIds.after.req, 'captured ids') assert(observedIds.after.res, 'captured ids') // data matches expectations assert.equal(observedIds.before.req, observedIds.before.res, 'ids match') //一開始兩個時相同的 assert.equal(observedIds.after.req, observedIds.after.res, 'ids match') //以後兩個的結果也是相同的,可是變成了newId // correct behavior assert.notEqual(observedIds.before.req, observedIds.after.req, 'ids are different') //先後的req.id不一樣了 assert.equal(observedIds.before.req, res.id, 'result id matches original') //可是before和最後輸出的結果res.id仍是同樣的 assert.equal(payload.id, res.id, 'result id matches original') assert.equal(payloadCopy.id, res.id, 'result id matches original') done() }) }) })
這裏能夠知道idRemapMiddleware的做用時在過程當中間獲得一個新的、比較複雜的Id進行一系列處理,可是最後輸出的給用戶看的表示結果仍是同樣的