mascara-2(MetaMask/mascara本地实现)-连接线上钱包
https://github.com/MetaMask/mascara
(beta) Add MetaMask to your dapp even if the user doesn't have the extension installed
可以开始分析一下这里的代码,从
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node example/server/" },
那么就从example/server/开始,这里有两个文件index.js和util.js:
index.js
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.js
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,去调用页面版
mascara/example/app/index.html
<!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>
再来就是
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文档的讲解了
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
}
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 () {}
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中查看这笔交易是否成功,发现是成功了的: