https://github.com/MetaMask/zero-clienthtml
MetaMask ZeroClient and backing iframe servicehtml5
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
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:
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,是第五次訪問