Posted at
EthereumDay 18

他のChrome拡張からMetaMaskを使う


前書き

今までEthereumのDapps開発には当然のようにMetaMaskを使っていましたが、今回自作のChrome拡張でMetaMaskを呼び出したくなり、呼び出せるようになるまで苦戦したので記事にしました。

(Chrome拡張を制作するのは今回が初めてなので、至らぬ点があるかもしれません。)


なんで他のChrome拡張からMetaMaskを呼び出せないの?

MetaMaskは開いているページのグローバルスコープのweb3という変数にWeb3インスタンスを格納し、Dapps開発者はこのインスタンスを使ってMetaMaskを使ってコントラクトの関数を実行したりします。

しかし、Chrome拡張は、「isolated world」というポリシーを持っており、ページのURLやDOM情報は取得・操作できても変数にはアクセスできないという仕様になっているためMetaMaskのWeb3インスタンスにアクセスすることができません。

しかし、私はMetaMaskのWeb3インスタンスにアクセスしたかった。。。


結論

MetaMaskのWeb3インスタンスにはアクセスできませんでした。

ただアクセスせずともChrome拡張からMetaMaskを呼び出せるようになりました。

Chrome拡張内で、MetaMaskのWeb3を使ってコントラクトを呼び出すスクリプトを書き、そのスクリプトを開いているページのDOMに追加して実行させることで実質的にMetaMask以外のChrome拡張からMetaMaskを操作することができます。


ページからスクリプトを実行してMetaMaskを呼び出す

MetaMaskがインストールされているページではweb3という変数にWeb3インスタンスがインジェクトされています。

そのためコンソールで以下のようなスクリプトを実行することでMetaMaskを用いてコントラクトのトランザクションを呼び出すことができます。


console.js

const contract = web3.eth.contract(contract_abi).at(contract_address);

contract.MyMethods.sendTransaction(params, {"from": web3.eth.accounts[0]}, (error, response)=>{
if(!error){
// 正しく終了した
} else {
// 問題発生
}
})

contract_abi, contract_address, MyMethods, paramsは自分のスマートコントラクトに応じたものを入れてください。

またMetaMaskのWeb3はバージョンが0.20.3なので1.0.0betaとは形式が異なることに注意してください。

このスクリプトを実行するとMetaMaskのポップアップが出てきて指定したスマートコントラクトを実行するトランザクションに署名を求められれば成功です。(MetaMaskのロックを解くのを忘れないでください。)

(MetaMask以外の)Chrome拡張からMetaMaskを呼び出すには、スマートコントラクトを実行したいタイミングでChrome拡張からページにこのスクリプトを埋め込んでやることでMetaMaskを呼び出すことができます。


Content ScriptからMetaMaskを呼び出す

Chrome拡張の記事ではないので詳しい説明は省きますが、Content Scriptというのは指定したページで動かせるスクリプトのことです。

例えばEnhancer for YouTubeはYoutubeの中で実行されるContent Scriptを持った拡張機能です。

Content Scriptは開いているページのDOMにアクセスすることはできますが、変数の値にはアクセスすることはできません。

よってMetaMaskのWeb3インスタンスにアクセスすることはできないのですが、DOMにアクセスすることができるため、先ほどのスクリプトをドキュメントに埋め込むコードを実行してやれば、MetaMaskを呼び出すことができます。


inject.js

var script1 = document.createElement("script");

script1.textContent = `var contract = web3.eth.contract(${JSON.stringify(contract_abi)}).at(${JSON.stringify(contract_address)})`;
document.body.appendChild(script1);

var script2 = document.createElement("script");
script2.textContent = `contract.${method}.sendTransaction(${JSON.stringify(param)}, {"from": web3.eth.accounts[0]}, (err,res)=>{})`;
document.body.appendChild(script2);


行が増えて複雑に見えますが、やっていることはscript要素を生成し、そこに先ほどのスクリプトを格納して、ドキュメントに追加しているだけです。


ポップアップからMetaMaskを呼び出す

Content Scriptの場合はDOMツリーを共有しているため、普通にスクリプトを埋め込むだけでMetaMaskを呼び出すことができますが、ポップアップ内で実行されるスクリプトは、開いているページのDOMツリーにアクセスできないので、普通にスクリプトを埋め込むこんでもMetaMaskを操作することはできません。

しかしchrome.tabs.executeScript()を使うとポップアップではなく、開いているページ内のスコープでスクリプトを実行することができます。

さっそく


executeScript1.js

// chrome.tabs.executeScriptを使うにはChromeの設定でtabsかactiveTabのパーミッションが必要です。

chrome.tabs.executeScript({ "code": `var contract = web3.eth.contract(${JSON.stringify(contract_abi)}).at(${JSON.stringify(address)})` });
chrome.tabs.executeScript({ "code": `contract.MyMethods.sendTransaction(${JSON.stringify(param)}, {"from": web3.eth.accounts[0]}, (err,res)=>{})` });

これを実行するとweb3という変数が定義されていないとエラーを返されます。

しかし、


executeScript2.js

chrome.tabs.executeScript({ "code": "console.log(document)" });


を実行すると開いているページのDOMツリーが出力されます。

どうやらContent Scriptのようにページの変数にはアクセスできないがDOMツリーにはアクセスできる仕様のようです。

よって、chrome.tabs.executeScriptを使ってDOMツリーにスクリプトを追加するスクリプトを実行すればポップアップからもMetaMaskを呼び出すことができます。


executeScript3.js

chrome.tabs.executeScript({ "code": `var script1 = document.createElement("script");` });

chrome.tabs.executeScript({ "code": `script1.textContent = 'var contract = web3.eth.contract(${JSON.stringify(contract_abi)}).at(${JSON.stringify(address)})'`});
chrome.tabs.executeScript({ "code": `document.body.appendChild(script1);` });

chrome.tabs.executeScript({ "code": `var script2 = document.createElement("script");` });
chrome.tabs.executeScript({ "code": `script2.textContent = 'contract.MyMethods.sendTransaction(${JSON.stringify(param)}, {"from": web3.eth.accounts[0]}, (err,res)=>{})'` });
chrome.tabs.executeScript({ "code": `document.body.appendChild(script2);` });



あとがき

以上のようにして他のChrome拡張からMetamaskを呼び出せるようになりました。

本当は、Ethereum Advent CalenderまでにChrome拡張を作ってここで記事を書く(宣伝する)つもりだったのですが間に合いませんでした。。。 できたらここに載せる予定です。