MetaMask/zero-client

https://github.com/MetaMask/zero-clienthtml

 

MetaMask ZeroClient and backing iframe servicehtml5

 

architecture

here is a comparison of the extension-based and iframe-based architecturenode

metamask extension:git

dapp inpage.js <-> forwarder contentscript.js <-> extension background.js
ui popup.js <-> extension background.js

就是當用戶經過瀏覽器經過web3去調用json rpc時,即經過dapp inpage.js,再傳給metamask contentscript,再傳給metamask background,background將會將要進行的操做傳給UI,使其可以在與rpc進行交互以前再一次獲得用戶的確認,用戶確認後就會返回background去與rpc進行交互,而後再將獲得的數據經過metamask contentscript-dapp inpage.js傳給瀏覽器github

 

current metamask iframe:web

dapp inpage.js <-> iframe background.js
ui app.metamask.io <-> iframe background.js

 

ideal metamask iframe:chrome

dapp inpage.js <-> iframe forwarder <-> sharedworker background.js
ui app.metamask.io <-> iframe forwarder <-> sharedworker background.js

 

ideal unified metamask extension+iframe:docker

dapp inpage.js <-> iframe forwarder <-> sharedworker background.js
ui popup.js <-> iframe forwarder <-> sharedworker background.js

 

example

npm start

 

運行:npm

首先:json

userdeMacBook-Pro:zero-client user$ npm install

而後運行npm start:

userdeMacBook-Pro:zero-client user$ npm start

> metamask-zeroclient@2.0.0 start /Users/user/zero-client
> ./example.sh

./example.sh: line 3: beefy: command not found
./example.sh: line 4: beefy: command not found
npm ERR! file sh
npm ERR! code ELIFECYCLE
npm ERR! errno ENOENT
npm ERR! syscall spawn
npm ERR! metamask-zeroclient@2.0.0 start: `./example.sh`
npm ERR! spawn ENOENT
npm ERR! 
npm ERR! Failed at the metamask-zeroclient@2.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/user/.npm/_logs/2018-11-12T09_26_56_269Z-debug.log

出錯,沒有安裝模塊beefy:

什麼是beefy及其使用

userdeMacBook-Pro:zero-client user$ npm install beefy --save

 

再運行又出錯:

userdeMacBook-Pro:zero-client user$ npm start

> metamask-zeroclient@2.0.0 start /Users/user/zero-client
> ./example.sh

beefy (v2.1.8) is listening on http://127.0.0.1:9001
beefy (v2.1.8) is listening on http://127.0.0.1:9002
200   58ms       261B  /
200   70ms     1.95KB  /bundle.js ➞ /Users/user/zero-client/node_modules/browserify /Users/user/zero-client/.index.js -d
Error: Cannot find module 'metamask-crx/app/scripts/lib/inpage-provider.js' from '/Users/user/zero-client/lib'
    at /Users/user/zero-client/node_modules/browser-resolve/node_modules/resolve/lib/async.js:46:17
    at process (/Users/user/zero-client/node_modules/browser-resolve/node_modules/resolve/lib/async.js:173:43)
    at ondir (/Users/user/zero-client/node_modules/browser-resolve/node_modules/resolve/lib/async.js:188:17)
    at load (/Users/user/zero-client/node_modules/browser-resolve/node_modules/resolve/lib/async.js:69:43)
    at onex (/Users/user/zero-client/node_modules/browser-resolve/node_modules/resolve/lib/async.js:92:31)
    at /Users/user/zero-client/node_modules/browser-resolve/node_modules/resolve/lib/async.js:22:47
    at FSReqCallback.oncomplete (fs.js:161:21)

發現是在lib/setup-provider.js中調用了metamask-crx模塊(metamask chrome extension),但node_modules中沒有

從該網址下載https://github.com/ATNIO/metamask-crx:

const MetamaskInpageProvider = require('metamask-crx/app/scripts/lib/inpage-provider.js')

再運行,仍是報錯:

Error: Cannot find module 'brfs' from '/Users/user/zero-client/node_modules/metamask-crx'
    at /Users/user/zero-client/node_modules/resolve/lib/async.js:51:31
    at processDirs (/Users/user/zero-client/node_modules/resolve/lib/async.js:185:39)
    at ondir (/Users/user/zero-client/node_modules/resolve/lib/async.js:200:13)
    at load (/Users/user/zero-client/node_modules/resolve/lib/async.js:83:43)
    at onex (/Users/user/zero-client/node_modules/resolve/lib/async.js:108:17)
    at /Users/user/zero-client/node_modules/resolve/lib/async.js:12:69
    at FSReqCallback.oncomplete (fs.js:161:21)

是由於下載下來的metamask-crx模塊忘記運行npm install

 

而後再運行就成功了:

userdeMacBook-Pro:zero-client user$ npm start

> metamask-zeroclient@2.0.0 start /Users/user/zero-client
> ./example.sh

beefy (v2.1.8) is listening on http://127.0.0.1:9001
beefy (v2.1.8) is listening on http://127.0.0.1:9002
200   50ms       261B  /
200   63ms     1.95KB  /bundle.js ➞ /Users/user/zero-client/node_modules/browserify /Users/user/zero-client/.index.js -d
200 2422ms     3.12MB  /zero.js ➞ /Users/user/zero-client/node_modules/browserify /Users/user/zero-client/index.js -d
200   32ms       427B  /
404    1ms        12B  /background.js
200 1497ms     2.33MB  /bundle.js ➞ ./node_modules/browserify ./frame.js -d
200   25ms       261B  /
200  116ms     1.95KB  /bundle.js ➞ /Users/user/zero-client/node_modules/browserify /Users/user/zero-client/.index.js -d
200 1720ms     3.12MB  /zero.js ➞ /Users/user/zero-client/node_modules/browserify /Users/user/zero-client/index.js -d
200    1ms       189B  /favicon.ico (generated)
200   13ms       261B  /
200   12ms     1.95KB  /bundle.js ➞ /Users/user/zero-client/node_modules/browserify /Users/user/zero-client/.index.js -d
200 1377ms     3.12MB  /zero.js ➞ /Users/user/zero-client/node_modules/browserify /Users/user/zero-client/index.js -d
200   16ms       427B  /

在沒有安裝metamask插件的瀏覽器上的結果是:

 

 

 

在安裝了metamask插件的瀏覽器上的結果是:

 

 

看到這個運行的結果後,咱們能夠看看其的代碼:

他的代碼與以前的mascara的差很少,若是想要看更詳細的解釋,可看mascara-2(MetaMask/mascara本地實現)-鏈接線上錢包

index.js

const Web3 = require('web3')
const setupProvider = require('./lib/setup-provider.js')

//
// setup web3
//

var provider = setupProvider()//1 設置provider
var web3 = new Web3(provider)
web3.currentProvider = provider
web3.setProvider = function(){
  console.log('MetaMask - overrode web3.setProvider')
}

//
// export web3
//
console.log(web3.currentProvider)
// console.log(web3.version.network)
// console.log(web3.eth.blockNumber)
global.web3 = web3

 

 zero-client/lib/setup-provider.js

const setupIframe = require('./setup-iframe.js')
const MetamaskInpageProvider = require('metamask-crx/app/scripts/lib/inpage-provider.js')

module.exports = getProvider


function getProvider(){//1

  if (global.web3) {//2若是有安裝插件,那麼就使用插件的web3
    console.log('MetaMask ZeroClient - using environmental web3 provider')
    return global.web3.currentProvider
  }
  //3若是沒有就本身設置
  console.log('MetaMask ZeroClient - injecting zero-client iframe!')
  var iframeStream = setupIframe({ //4設置iframe
    zeroClientProvider: 'http://localhost:9001', //4服務器端爲9001
    sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'],
  })

  var inpageProvider = new MetamaskInpageProvider(iframeStream) //6根據iframe流去設置MetamaskInpageProvider
  return inpageProvider

}

 

zero-client/lib/setup-iframe.js

const Iframe = require('iframe')
const IframeStream = require('iframe-stream').IframeStream

module.exports = setupIframe


function setupIframe(opts) {//4
  opts = opts || {}
  var frame = Iframe({
    src: opts.zeroClientProvider || 'https://zero.metamask.io/', //5就是若是沒有設置服務器,那麼就鏈接zero.metamask.io(docker設置,下面有講)
    container: document.head,
    sandboxAttributes: opts.sandboxAttributes || ['allow-scripts', 'allow-popups'],
  })
  var iframe = frame.iframe
  var iframeStream = new IframeStream(iframe)//5 而後建立iframe流

  return iframeStream
}

 

metamask-crx/app/scripts/lib/inpage-provider.js

const pipe = require('pump')
const StreamProvider = require('web3-stream-provider')
const LocalStorageStore = require('obs-store')
const ObjectMultiplex = require('./obj-multiplex')
const createRandomId = require('./random-id')

module.exports = MetamaskInpageProvider

function MetamaskInpageProvider (connectionStream) {//7
  const self = this

  // setup connectionStream multiplexing
  var multiStream = self.multiStream = ObjectMultiplex()//8生成多路複用流
  pipe(//8將多路複用流與iframe流鏈接
    connectionStream,
    multiStream,
    connectionStream,
    (err) => logStreamDisconnectWarning('MetaMask', err)
  )

  // subscribe to metamask public config (one-way)
  self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })//8設置本地存儲
  pipe(//8在多路複用流中添加publicConfig流用以接收本地存儲信息
    multiStream.createStream('publicConfig'),
    self.publicConfigStore,
    (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
  )

  // connect to async provider
  const asyncProvider = self.asyncProvider = new StreamProvider()//8設置rpc provider流
  pipe(
    asyncProvider,
    multiStream.createStream('provider'),
    asyncProvider,
    (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
  )

  self.idMap = {}
  // handle sendAsync requests via asyncProvider
  self.sendAsync = function (payload, cb) {
    // rewrite request ids
    var request = eachJsonMessage(payload, (message) => {//9 10 transformFn即這裏的回調參數,payload爲message
      var newId = createRandomId()//11這裏是將json rpc中的id隨機生成一個更復雜的id值去指明該筆request
      self.idMap[newId] = message.id //11保留舊的id,由於以後返回時返回的仍是舊ID,新id只在過程當中使用
      message.id = newId
      return message
    })
    // forward to asyncProvider
    asyncProvider.sendAsync(request, function (err, res) {
      if (err) return cb(err)
      // transform messages to original ids
      eachJsonMessage(res, (message) => {
        var oldId = self.idMap[message.id]
        delete self.idMap[message.id]
        message.id = oldId
        return message
      })
      cb(null, res)
    })
  }
}

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
      break

    case 'eth_uninstallFilter':
      self.sendAsync(payload, noop)
      result = true
      break

    case 'net_version':
      let networkVersion = self.publicConfigStore.getState().networkVersion
      result = networkVersion
      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.sendAsync = function () {
  throw new Error('MetamaskInpageProvider - sendAsync not overwritten')
}

MetamaskInpageProvider.prototype.isConnected = function () {
  return true
}

MetamaskInpageProvider.prototype.isMetaMask = true

// util

function eachJsonMessage (payload, transformFn) {//9
  if (Array.isArray(payload)) {//9若是是數組,說明一次要執行多個request
    return payload.map(transformFn)//9因此將數組中的request一個個傳入transformFn,最後傳出結果的數組
  } else {
    return transformFn(payload)//9若是不是,則說明是一個request,直接作transformFn的參數便可
  }
}

function logStreamDisconnectWarning(remoteLabel, err){
  let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
  if (err) warningMsg += '\n' + err.stack
  console.warn(warningMsg)
}

function noop () {}

 

從上面的代碼中咱們能夠看見,若是沒有設置provider的話(zeroClientProvider: 'http://localhost:9001'),那麼就會默認爲https://zero.metamask.io/

這是經過docker進行設置的,以下:

 Dockerfile

FROM node:0.12
MAINTAINER kumavis

# setup app dir
RUN mkdir -p /www/
WORKDIR /www/

# install dependencies
COPY ./package.json /www/package.json
RUN npm install

# copy over app dir
COPY ./ /www/

# run tests
RUN npm test

# start server
CMD npm start

# expose server
EXPOSE 9000

 從這裏咱們能夠看出基本上就是本文檔的複製粘貼,部署了一個服務器,並提供了9000做爲端口

 

 docker-compose.yml

zeroClient:
  image: kumavis/zeroclient
  restart: always
  environment:
    VIRTUAL_HOST: "zero.metamask.io"
    VIRTUAL_PORT: "9000"
    PORT: "9000"
  ports:
    - "9000"

 由於該項目的做者已經將該鏡像上傳,因此咱們能夠根據 docker-compose.yml的設置直接拉取鏡像進行使用:

 

userdeMBP:zero-client user$ docker-compose up
Pulling zeroClient (kumavis/zeroclient:)...
latest: Pulling from kumavis/zeroclient
d4bce7fd68df: Pull complete
a3ed95caeb02: Pull complete
816152842605: Pull complete
5dcab2c7e430: Pull complete
dc54ada22a60: Pull complete
f4dbf915606d: Pull complete
2e6a016344c7: Pull complete
310ce789aae0: Pull complete
d9f04797303f: Pull complete
e69d1d8ba6cd: Pull complete
508a0e5f7217: Pull complete
Digest: sha256:6a7936b08541ced92fc35f63033046942df09e6a48a56f199de315eac002be51
Status: Downloaded newer image for kumavis/zeroclient:latest
Creating zero-client_zeroClient_1 ... done
Attaching to zero-client_zeroClient_1
zeroClient_1  | 
zeroClient_1  | > metamask-zeroclient@1.0.0 start /www
zeroClient_1  | > node server.js
zeroClient_1  | 
zeroClient_1  | MetaMask ZeroClient iframe server listening on 9000

 

而後這時候將zero-client/lib/setup-provider.js中的http://localhost:9001去掉,讓其鏈接https://zero.metamask.io/

獲得結果:

 

 

而後在shared-worker文件夾中咱們可以看到有:

zero-client/shared-worker/client.js

var worker = new SharedWorker("worker.js");

var id = Number.MAX_SAFE_INTEGER*Math.random()//3 隨機生成id值

worker.port.addEventListener("message", function(e) {
  console.log(e)
  console.log(id)
  console.log("message")
  console.log(e.data);
}, false);

worker.port.start();//1 激活端口

// post a message to the shared web worker
worker.port.postMessage(id)//3 發送id,觸發server.js"message"事件

 擴展,參考https://www.jb51.net/html5/551063.html:

SharedWorker類(html5)

  SharedWorker的實質在於share,不一樣的線程能夠共享一個線程,他們的數據也是共享的。

本例子是經過addEventListener()方法監聽message事件,須要worker.port.start()方法激活端口。

也有下面這樣的寫法:

worker.port.onmessage = function(e) {
  console.log(e)
  console.log(id)
  console.log("message")
  console.log(e.data);
}

使用事件句柄的方式將聽message事件,不須要調用worker.port.start()

不一樣於worker,當有人和他通訊時觸發connect事件(以下),而後他的message事件是綁定在messagePort對象上的

zero-client/shared-worker/server.js

var connections = 0 // count active connections

self.addEventListener("connect", function (e) {//2 進行鏈接var port = e.ports[0]
  connections++

  port.addEventListener("message", function (e) {//4 收到消息id = e.data
    port.postMessage("Hello " + e.data + " (port #" + connections + ")")//5 post回消息,而後觸發client.js的"message"事件
  }, false)

  port.start()//2 打開端口

}, false)

run example

beefy client.js:index.js server.js:worker.js --live --open 

而後咱們爲了將這個例子運行起來,須要對package.json進行更改,添加"example"

  "scripts": {
    "test": "echo \"Error: no tests yet\" && exit 0",
    "start": "./example.sh",
    "example": "beefy shared-worker/client.js:index.js shared-worker/server.js:worker.js 9003 --live --open "
  },

 這裏沒有設置端口,會隨機使用一個端口,可是你也能夠本身設置,這裏咱們設置爲9003,而後運行:

userdeMacBook-Pro:zero-client user$ npm run example

> metamask-zeroclient@2.0.0 example /Users/user/zero-client
> beefy shared-worker/client.js:index.js shared-worker/server.js:worker.js 9003 --live --open 

beefy (v2.1.8) is listening on http://127.0.0.1:9003
200   70ms       552B  /
200  793ms     2.36KB  /index.js ➞ ./node_modules/browserify ./shared-worker/client.js -d
200   30ms       552B  /
200   17ms     2.36KB  /index.js ➞ ./node_modules/browserify ./shared-worker/client.js -d
200   36ms       552B  /
200   10ms     2.36KB  /index.js ➞ ./node_modules/browserify ./shared-worker/client.js -d
200   19ms       552B  /
200    8ms     2.36KB  /index.js ➞ ./node_modules/browserify ./shared-worker/client.js -d

 而且將index.html上的bundle.js改成index.js:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>MetaMask ZeroClient Iframe</title>
  <span style="white-space:pre"></span>
  <link href="http://www.lituanmin.com/favicon.ico" rel="icon" type="image/x-icon" />
  <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="/index.js"></script>
</body>
</html>

 返回:

 刷新一次能夠看見port 4變成了port 5,是第五次訪問

 

本站公眾號
   歡迎關注本站公眾號,獲取更多信息