こんにちは。
ここ2カ月ほど前から仮想通貨をいじり始めてすっかり仮想通貨のことしか考えられなくなってしまった者です。
ブロックチェーンとかNFTとかスマートコントラクトとか、単語は知っていたけど全く興味がなかったんですが、仮想通貨を触っていると技術的な部分にも興味が出てきました。
正直イーサチェーンのガス代の高さに辟易としているので、別のチェーンのスマートコントラクトを扱える言語があるならそっちをやりたいとは思うんですが、まあ考え方は一緒だろうと思い、とりあえず情報の多いSolidityを選びました。(後で調べたらVelasチェーンとかもSolidityで開発できるっぽい多分)
なので私は**「よし、じゃあまず開発環境を作ろう!」**と思い、Solidityへの入門は一旦置いておいて、まず開発環境を作ることにしました。
先に成果物
この記事で扱うバージョン
Solidity v0.8.10
geth v1.10.12
バージョンによって全然情報が違うのでものすごい苦労しました。
なのでググる時は注意して下さい。
gethの導入
なにはともあれ、まずはイーサチェーンを再現してくれる環境が必要だなあと思い、ググるとgethというソフトがあり、既にDockerHubにgethを組み込んだイメージがあるのでそれを使うことにしました。
一応これだけあれば、execでDockerの中に入ってコマンドラインでweb3を実行してイーサチェーンを試すことができますが、使い勝手が悪いのでhttpサーバーとして実行することにします。
最初起動オプションが多すぎて考えるのをやめそうになりましたが、いろいろ試行錯誤した結果このようになりました。
ENTRYPOINT geth \
--dev \
--http \
--http.addr '0.0.0.0' \
--http.port 8545 \
--http.api 'eth,web3,personal,net' \
--http.corsdomain '*' \
--http.vhosts '*' \
--ws \
--ws.addr '0.0.0.0' \
--ws.port 8546 \
--ws.api 'eth,web3,personal,net' \
--ws.origins '*' \
--allow-insecure-unlock
オプションについて
- --dev
- これがないと実際のイーサネットのどこかに繋がってしまう
- あとから気づくことになるが、データが保存されないのでDockerを落とすとデータが消えます(--datadirで指定したら残るかも?)
- --http系
- httpサーバーとして起動する
- addrを指定しないと外部から通信できない
- 他は何を許可するかのオプション
- --ws系
- WebSocketを開く
- httpと同様addrを指定しないと外部から通信ができない
- 他は何を許可するかのオプション
- --allow-insecure-unlock
- httpからは実行できないAPIがあるが、それを回避してくれる
- もしかしたらこれがあればhttpだけで完結するのでwsの指定はいらないかも?
- 本番環境だとやっぱり実行できないといった可能性もあるのでちゃんと調べた方がよさそう
Solidityの導入
次にSolidityがインストールされたイメージが必要ですが、調べるとあったのでそのまま使いました。
実際の環境と同じくイーサチェーンに対して外部からアクセスする想定なので、イメージを分けます。
また、後述する自動化のために必要なNode.jsも入れました。
ついでにReactも導入
webアプリ側の開発も1か所でできれば楽じゃん!という安易な発想でもう一つReact用のイメージを作りました。
この辺りは特には触れません。
出来上がったdocker-compose.yml
version: '3'
services:
eth:
build:
context: .
dockerfile: ./docker/images/eth/Dockerfile
ports:
- 8545:8545
- 8546:8546
- 30303:30303
tty: true
networks:
default:
sckit-network:
sol:
build:
context: .
dockerfile: ./docker/images/sol/Dockerfile
env_file: ./.env
entrypoint: npm run watch:sol
volumes:
- ./:/projects/sckit:cached
- /projects/sckit/node_modules
environment:
- CHOKIDAR_USEPOLLING=true
tty: true
networks:
default:
sckit-network:
web:
build:
context: .
dockerfile: ./docker/images/web/Dockerfile
command: npm run start
env_file: ./.env
volumes:
- ./:/projects/sckit:cached
- /projects/sckit/node_modules
environment:
- CHOKIDAR_USEPOLLING=true
ports:
- 32334:3000
networks:
default:
sckit-network:
networks:
sckit-network:
external: true
見直すと余計な記述もありますがそのままです。
volumes:
- /projects/sckit/node_modules
docker build時にnpm installするようにしたので、この部分でホスト側からのマウントによるnode_modulesの上書きを回避しているのがミソです。
自動化させていく
開発効率を上げるためには、
- Solidityのsolファイルの自動ビルド
- 出来上がったコントラクトの自動デプロイ
- web3などからコントラクトの実行
をスムーズに行える必要があります。
更にスマートコントラクトを実行するためのガス代の調達なども必要です。
Remixとかを使えば楽なのかもしれませんが、いきなりRemix見ても何が何やらだったのですぐやめたという経緯があります。
なので、ラッピングされてしまっている部分を知るたの開発環境の構築でもあります。
Solidityのsolファイルの自動ビルド
さて、普段Node.jsそのもので何か開発することがないので、ここからは普段からNode.jsで開発をしてる人からすると低レベルな話になります。
自動ビルドということで、solファイルに変更があった際にまずそれを検知したいので、
chokidarというライブラリがあるようなのでそれを入れました。
const watcher = chokidar.watch('src', {
persistent: true // ファイルを監視し続けるか
});
// 変更検知時の設定
watcher.on('change', (path, stats) => {
// ...
});
ビルド自体はSolidityのイメージにビルド用のコマンドが用意されているので、そちらを使います。
solc --bin --abi HelloWorld.sol
これをファイルの変更が検知されるたびに行いたいので、Node.js組み込みのchild_processをインポートしてexec関数を利用します。このexec関数で実行したスクリプトの中で実際にビルドを行っています。
exec.exec('npm run build:sol');
ビルドを行うと、コントラクトのバイナリデータとABI(Application Binary Interface)が生成されます。
なかなか見るのが嫌になる文字数です。
尚、今回利用したsolファイルの内容はこちらです。
// HelloWorld.sol
pragma solidity ^0.8.10;
contract HelloWorlda {
function getSender() public returns (address) {
return msg.sender;
}
function getBalance() public returns (uint256) {
return address(msg.sender).balance;
}
}
実行者のアドレスと、実行者の残高を返すだけのシンプルな内容です。
生成されたバイナリデータとABIはデプロイ時とコントラクト実行時に指定する必要があるので、
実行結果をパースして適当な変数に入れておきます。
出来上がったコントラクトの自動デプロイ
web3のAPIを使って、geth上にデプロイを行います。
ググったりしても情報が古かったりで、デプロイに一番苦労した気がします。
const web3 = new Web3('http://eth:8545');
const contract = new web3.eth.Contract(JSON.parse(abi)); // さっき変数に入れたABIを指定してコントラクト作成
return contract
.deploy({
data: binary // デプロイ時にはバイナリデータが必要になるらしい
})
.send({
from: from, // コントラクト作成者のアドレスが入る
gas: 1500000 // ここよく調べてないのでこの値の根拠は不明
})
.then(async (newContractInstance) => {
return newContractInstance.options.address; // これがコントラクトのアドレスになる
});
ここで出てきたのがコントラクト作成者のアドレスです。
仮想通貨界隈ではアドレス=ウォレットのアドレス=アカウントと考えていいと思います。
--devオプションでgethを実行すると、coinbaseと呼ばれるgethネットワーク自体の大元のアカウントが自動で作成されるのでそれを指定しています。
なのでこれはgethが発行したコントラクトということになります。
デプロイが完了するとコントラクトのアドレスが払い出されます。
このアドレスに対して我々はウォレットを接続してなんやかんややっているというわけです。
web3などからコントラクトの実行
できたコントラクトを実際に実行していきたいと思います。
import helloWorldContract from '../built/HelloWorldContract';
const web3 = new Web3('http://eth:8545');
web3.eth.defaultAccount = await getMyEthAccount();
const contract = new web3.eth.Contract(
helloWorldContract.abi, // ★
helloWorldContract.address // ★
);
await contract.methods.getSender()
.call()
.then((response: any) => {
return response;
});
この★マークのついているところですが、デプロイする度にアドレスが変わるので、自動化等をしない場合はnew web3.eth.Contract()
の中に入れる文字列を、毎回直接コピペして入れてあげる必要があります。
そんなことは毎回やっていられないので、ビルド時の実行結果を保持したtsファイルも自動生成しています。
ここではNode.js組み込みのfs関数を使って書き出しています。
fs.writeFileSync(
`./built/${fineName}`,
`import { ContractJsonType } from '../libs/contractJsonType';
const ${varName}: ContractJsonType = ${JSON.stringify(json, null, 2)}
export default ${varName};
`
);
クッソ雑で申し訳ないレベルですが、これを行った結果が次のようなtsファイルになります。
// HelloWorldContract.ts
import { ContractJsonType } from '../libs/contractJsonType';
const helloWorldContract: ContractJsonType = {
"address": "0xcd277fa685Bef4caD6EA0F61354D5De4E2F429f3",
"abi": [
{
"inputs": [],
"name": "getBalance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getSender",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
}
export default helloWorldContract;
これであれば、デプロイする度にこの内容が変化したとしても使う側はaddressとabiを呼ぶだけで済みます。
ここまでがスマートコントラクトを自動デプロイしてくれるSolidityの開発環境の全容です。
余談
実際にはcoinbaseアドレスだけではなく、自身の代わりとなるアドレスと、ガス代となるETHも必要なのでそれもコマンド一発で付与できるようになっています。
アカウント作成
axios.post(
'http://eth:8545',
{
'jsonrpc': '2.0',
'method': 'personal_newAccount',
'params': ['testpassword']
}
)
.then(response => {
console.log('Create success!');
})
.catch(error => {
throw new Error(error);
});
ETH付与
return axios.post(
'http://eth:8545',
{
'jsonrpc': '2.0',
'method': 'eth_sendTransaction',
'params': [
{
'from': from, // coinbaseアドレス。coinbaseはデフォでETHを大量に持っているので、そこから貰う
'to': to, // 自分のアドレス
"value": hexAmount // 付与する量
}
],
'id': 1
}
)
.then(response => {
console.log('Charged!');
})
.catch(error => {
throw new Error(error);
});
そもそもどこからETHを拾えばいいんだ・・・?
という感じで最初はどうしていいかよくわからなかったです。
あと作ってから気づいたのが、axiosじゃなくてweb3から全部操作すれば済む話でした。
直すのがめんどくさい(小声)
アカウントの確認
axios.post(
'http://eth:8545',
{
'jsonrpc': '2.0',
'method': 'eth_accounts',
'params': [],
'id': 1
}
)
これの結果は下記のようになります。
[ '0xf59dd36d02a80860fcce590e303642315097d18a',
'0x7313f2032b16785720e8f5566b89422972090f9d' ]
配列で取れてくるのですが、0番目がcoinbaseのアドレス、1番目が先ほど作成した自分のアドレスになります。
基本的にはこの開発環境では1番目のアドレスを自分のアドレスということにして作っています。
あとがき
今回の内容に関してはSolidity以外でも応用が利くので、自分の中ではいい経験になりました。
現状かなり粗削りなので、実際にSolidityによる開発を進めていく中で、もう少しこうしたほうが便利といった改善点がたくさん出てくると思います。
また、Solidityに関してはかなり初心者には参入障壁が高いなあと感じました。
まず仮想通貨をある程度触らないとどこでどう使われるのかイメージがつかなかったり、仮想通貨界隈で頻出する用語とか仕様もあるので、まずは仮想通貨自体を持って運用してみるところから始めるととっつきやすいんじゃないかと思いました。
あと、とにかくSolidityがバージョンによって書き方が違いすぎるので、調べるのにやたら苦労します。
今回はSolidityの内容に関してはフォーカスしていませんが、やる気があったら記事にしていきたいと思います。
ないとは思いますがプルリク等もいただけると嬉しいです!
以上。