mascara-2(MetaMask/mascara本地實現)-鏈接線上錢包

 https://github.com/MetaMask/mascarajavascript

 (beta) Add MetaMask to your dapp even if the user doesn't have the extension installedhtml

 能夠開始分析一下這裏的代碼,從package.json中咱們能夠看到start中的內容: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

mascara/example/app.js瀏覽器

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文檔的講解了

mascara/lib/setup-provider.js

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 }

 

mascara/lib/setup-iframe.js

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

 

 mascara/lib/setup-widget.js

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中查看這筆交易是否成功,發現是成功了的:

相關文章
相關標籤/搜索