maybe daily dev notes

私の開発日誌

Hardhat/Truffleを使わずにSolidityをコンパイルする

Hardhat/Truffle を使わずに、Solidityのスマートコントラクトをコンパイルしたい。そういった望みを持つこともあると思う。 そんなときに使える方法を紹介する。

solc-js

solc-js という solc (Solidityのコンパイラ) をJavaScriptからプログラマブルに呼び出せるようラップしたライブラリがある。 今回はこれを使ってSolidityのスマートコントラクトをコンパイルする。

github.com

インストール

solc-jsのインストールはnpmだと次のコマンドでできる:

npm i solc

なお、solc-jsには現状TypeScript用の型定義ファイルが同梱されていない。Type Definition for solc-js ? · Issue #578 · ethereum/solc-js · GitHub

このため、このままでは TypeScriptの記法で import することはできない。これを回避するにはいくつかの方法がある (以下を参照) ので、都合の良いものを採用しよう。

stackoverflow.com

以下では、JSの記法 const solc = require('solc') でインポートするようにする。将来solc-jsのTS対応が完了すれば型定義も同梱されるようなので、その時に直せば良いだろう。

使い方

ここまでできたら、次のようなコードでSolidityをコンパイルできる。

import * as fs from 'fs';
const solc = require('solc');

const compile = async (fileName: string, contractName: string) => {
  const file = fs.readFileSync(fileName).toString();
  const output = 'input.sol';
  const input = {
    language: 'Solidity',
    sources: {
      [output]: {
        content: file,
      },
    },
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      outputSelection: {
        [output]: {
          '*': ['evm.bytecode.object', 'abi', 'evm.deployedBytecode'],
        },
      },
    },
  };

  const out = JSON.parse(solc.compile(JSON.stringify(input), { import: findImports }));
  const contract = out.contracts[output][contractName];
  return { bytecode: `0x${contract.evm.bytecode.object}`, abi: contract.abi };
};

const importCache: { [key: string]: { contents: string } } = {};
const findImports = (path: string) => {
  if (importCache[path] == null) {
    const file = fs.readFileSync(`node_modules/${path}`);
    importCache[path] = {
      contents: file.toString(),
    };
  }
  return importCache[path];
};

// コンパイル実行
compile("sample.sol", "DemoContract");

基本的には solc.compile という関数に solc のStandard input jsonを渡せば良い。 上記コードでは input 変数がそれに当たり、ここを上記ドキュメントに沿って変更することで、任意の構成でコンパイルを実行できる。

ただし、実世界のSmart contractは何らかのライブラリに依存することが多い。その場合は、それぞれのimportを解決する必要がある。

importの解決のために、 { import: findImports } というオプションで解決の関数を渡す。findImports 関数は独自に実装する必要があるが、依存するファイルをローカルに用意しておけば、単にファイルを読み込んで返すだけで済む。

例えば、以下のようなimportを解決したい場合、

import "@openzeppelin/contracts/access/Ownable.sol";

事前に npm install @openzeppelin/contracts を実行すれば、 ./node_modules/@openzeppelin/contracts/contracts/access/Ownable.sol を読み込むだけでimport先のファイルを取得できる。

上記コードの findImports 関数でも、そのような処理を実行している。

コンパイルするまで依存するライブラリを予測できない場合は、findImportsの中でネットワーク越しに依存ライブラリを取得するような処理が必要になる。(けど、そんなユースケースは少なそう)

これだけでコンパイルができた。なお上のコードでは、最後にBytecodeとABIを返すようにしているが、これも standard output json を参照すれば任意の項目を返すようにカスタマイズできる。

上記のコードはまとめて以下のリポジトリに配置した。

github.com

その他

コンパイラのバージョンは以下で取得可能。基本的に solc-js 自体のバージョンと同じになる模様。

const solc = require('solc');
const version = solc.version();  // '0.8.13+commit.abaa5c0e.Emscripten.clang'

もしコンパイラのバージョンを変えたいときは、こちら: https://github.com/ethereum/solc-js#using-a-legacy-version

まとめ

Solidityのコンパイルくらいであれば、重厚なライブラリを使わずとも比較的簡単に実現できる。 ユースケースによってはこれが活きることもあるので、活用されたい。