metamask源碼學習-inpage.js

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

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

 

 

 

RpcEngine——MetaMask/json-rpc-engine

https://github.com/MetaMask/json-rpc-engine

a tool for processing JSON RPC

usage

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

 

gotchas陷阱

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進行一系列處理,可是最後輸出的給用戶看的表示結果仍是同樣的

相關文章
相關標籤/搜索