Solidityとは
- Ethereum(イーサリアム)の共同設立者のギャビン・ウッドによって開発されたプログラミング言語です。
- Ethereumとはブロックチェーンプラットフォームです。時価総額2位(2020年2月時点)の仮想通貨
特徴は
- 初心者でも扱いやすい
- オブジェクト指向のプログラミング言語
- スマートコントラクト作成機能を備えている
- スマートコントラクトとは決まった条件下での契約の自動執行を指します。
- 例:自動販売機の自動執行される売買契約
環境構築
必要なツール
- Node.js
- Ganache(ガナッシュ)
- ローカル開発時に Ethereum プライベートブロックチェーンを可視化するツール
- Truffle
- Ethereum アプリケーション開発におけるフレームワーク
- MetaMask
- イーサリアムブロックチェーンに対応した仮想通貨ウォレット
前提条件
- Node.jsはインストール済みであること
- MetaMaskについては触れません。
Truffleのインストール
- コマンドは以下
$ npm install -g truffle
- インストール後、バージョン確認
- 2023/09/18時点でのバージョンは 5.11.5 でした
$ truffle version
Truffle v5.11.5 (core: 5.11.5)
Ganache v7.9.1
Solidity v0.5.16 (solc-js)
Node v18.17.1
Web3.js v1.10.0
Ganacheのインストール
-
以下公式サイトにアクセスし、インストーラをダウンロード
https://trufflesuite.com/ganache/
クイックスタート
- 公式サイトのクイックスタートを参考に実施します
プロジェクトの作成
% truffle unbox metacoin
This directory is non-empty...
? Proceed anyway? (Y/n)
Starting unbox...
=================
? Proceed anyway? Yes
✓ Preparing to download box
✓ Downloading
✓ Cleaning up temporary files
✓ Setting up box
Unbox successful, sweet!
Commands:
Compile contracts: truffle compile
Migrate contracts: truffle migrate
Test contracts: truffle test
- 以下ディレクトリ構成でプロジェクトが作成されます。
% tree
.
├── LICENSE
├── contracts
│ ├── ConvertLib.sol
│ └── MetaCoin.sol
├── migrations
│ └── 1_deploy_contracts.js
├── quick-start-metacoin
├── test
│ ├── TestMetaCoin.sol
│ └── metacoin.js
└── truffle-config.js
5 directories, 7 files
contracts/MetaCoin.sol
- MetaCoin.solというコントラクタが作成されています。
- 見るべきポイントとしてsendCoin関数を抑えておく
- sendCoin関数:
- 他のアドレスにコインを送信。
- 送金完了時、送信元の残高から送金額を減少させる。送信先の残高に加算する
- Transferイベントを発火。トランザクションの詳細を記録
// SPDX-License-Identifier: MIT
// Tells the Solidity compiler to compile only from v0.8.13 to v0.9.0
pragma solidity ^0.8.13;
import "./ConvertLib.sol";
// This is just a simple example of a coin-like contract.
// It is not ERC20 compatible and cannot be expected to talk to other
// coin/token contracts.
contract MetaCoin {
mapping (address => uint) balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
constructor() {
balances[tx.origin] = 10000;
}
function sendCoin(address receiver, uint amount) public returns(bool sufficient) {
if (balances[msg.sender] < amount) return false;
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Transfer(msg.sender, receiver, amount);
return true;
}
function getBalanceInEth(address addr) public view returns(uint){
return ConvertLib.convert(getBalance(addr),2);
}
function getBalance(address addr) public view returns(uint) {
return balances[addr];
}
}
contracts/ConvertLib.sol
- MetaCoin.solにてアドレス残高取得ロジックを本コントラクトのconvert関数で実現
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// A library is like a contract with reusable code, which can be called by other contracts.
// Deploying common code can reduce gas costs.
library ConvertLib{
function convert(uint amount, uint conversionRate) public pure returns (uint convertedAmount)
{
return amount * conversionRate;
}
}
migrations/1_deploy_contracts.js
- デプロイスクリプト
- ConvertLib、MetaCoinコントラクトをデプロイする
const ConvertLib = artifacts.require("ConvertLib");
const MetaCoin = artifacts.require("MetaCoin");
module.exports = function(deployer) {
deployer.deploy(ConvertLib);
deployer.link(ConvertLib, MetaCoin);
deployer.deploy(MetaCoin);
};
test/TestMetaCoin.sol
- contracts/MetaCoin.sol のテストコントラクト
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// These files are dynamically created at test time
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MetaCoin.sol";
contract TestMetaCoin {
function testInitialBalanceUsingDeployedContract() public {
MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin());
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
function testInitialBalanceWithNewMetaCoin() public {
MetaCoin meta = new MetaCoin();
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
}
test/metacoin.js
- JavaScriptで書かれたテストプログラム
const MetaCoin = artifacts.require("MetaCoin");
contract('MetaCoin', (accounts) => {
it('should put 10000 MetaCoin in the first account', async () => {
const metaCoinInstance = await MetaCoin.deployed();
const balance = await metaCoinInstance.getBalance.call(accounts[0]);
assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account");
});
it('should call a function that depends on a linked library', async () => {
const metaCoinInstance = await MetaCoin.deployed();
const metaCoinBalance = (await metaCoinInstance.getBalance.call(accounts[0])).toNumber();
const metaCoinEthBalance = (await metaCoinInstance.getBalanceInEth.call(accounts[0])).toNumber();
assert.equal(metaCoinEthBalance, 2 * metaCoinBalance, 'Library function returned unexpected function, linkage may be broken');
});
it('should send coin correctly', async () => {
const metaCoinInstance = await MetaCoin.deployed();
// Setup 2 accounts.
const accountOne = accounts[0];
const accountTwo = accounts[1];
// Get initial balances of first and second account.
const accountOneStartingBalance = (await metaCoinInstance.getBalance.call(accountOne)).toNumber();
const accountTwoStartingBalance = (await metaCoinInstance.getBalance.call(accountTwo)).toNumber();
// Make transaction from first account to second.
const amount = 10;
await metaCoinInstance.sendCoin(accountTwo, amount, { from: accountOne });
// Get balances of first and second account after the transactions.
const accountOneEndingBalance = (await metaCoinInstance.getBalance.call(accountOne)).toNumber();
const accountTwoEndingBalance = (await metaCoinInstance.getBalance.call(accountTwo)).toNumber();
assert.equal(accountOneEndingBalance, accountOneStartingBalance - amount, "Amount wasn't correctly taken from the sender");
assert.equal(accountTwoEndingBalance, accountTwoStartingBalance + amount, "Amount wasn't correctly sent to the receiver");
});
});
テストプログラムを実行
- テストプログラムも用意されているので、テスト実行で動作確認してみます。
- 以下コマンドを実行
% truffle test
Compiling your contracts...
===========================
✓ Fetching solc version list from solc-bin. Attempt #1
✓ Downloading compiler. Attempt #1.
> Compiling ./contracts/ConvertLib.sol
> Compiling ./contracts/MetaCoin.sol
> Compiling ./test/TestMetaCoin.sol
> Artifacts written to /var/folders/ms/30d25_0x5lv94y_5_pcp9f340000gn/T/test--5064-PHriPnpBOths
> Compiled successfully using:
- solc: 0.8.13+commit.abaa5c0e.Emscripten.clang
TestMetaCoin
✔ testInitialBalanceUsingDeployedContract
✔ testInitialBalanceWithNewMetaCoin
Contract: MetaCoin
✔ should put 10000 MetaCoin in the first account
✔ should call a function that depends on a linked library (41ms)
✔ should send coin correctly (60ms)
5 passing (3s)
Ganacheとリンク
Ganacheの NEW WORKSPACE ボタンをクリック
上記で作成したプロジェクトのtruffle-config.jsを定義し、 START ボタンをクリック
作成されました
migrate実行
- プロジェクトのディレクトリのターミナルで以下コマンド実行
- コンパイルされたコントラクトがブロックチェーンにデプロイされたことが確認できています。
- 末尾のサマリーで確認すると以下コントラクトがデプロイされ、コストも明記されています。
- コントラクト: MetaCoin, ConvertLib
- コスト: 0.001894367934056634 ETH
% truffle migrate
Compiling your contracts...
===========================
> Compiling ./contracts/ConvertLib.sol
> Compiling ./contracts/MetaCoin.sol
> Artifacts written to /Users/nagaseyutaka/Dev/800-private/14-solidity-study/build/contracts
> Compiled successfully using:
- solc: 0.8.13+commit.abaa5c0e.Emscripten.clang
Starting migrations...
======================
> Network name: 'ganache'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)
1_deploy_contracts.js
=====================
Deploying 'ConvertLib'
----------------------
> transaction hash: 0xe4bac39d47eab771be075d84fea3ed51d8bd2209a890ddb61468220dc6dbcac9
> Blocks: 0 Seconds: 0
> contract address: 0xbAd967CF31a5Eb809D1c3df7E74d62EaF254c84f
> block number: 1
> block timestamp: 1695026534
> account: 0x0433811BAd4c494f8EeAB84886F2d1d85c80fAdA
> balance: 99.999468208
> gas used: 157568 (0x26780)
> gas price: 3.375 gwei
> value sent: 0 ETH
> total cost: 0.000531792 ETH
Linking
-------
* Contract: MetaCoin <--> Library: ConvertLib (at address: 0xbAd967CF31a5Eb809D1c3df7E74d62EaF254c84f)
Deploying 'MetaCoin'
--------------------
> transaction hash: 0x54e9c9def1b25dd16c8cda0666324bd86032ccb18aa9559ca3e5a18351fb9c4b
> Blocks: 0 Seconds: 0
> contract address: 0x1e013b453Ed87691545F1173ba7D97F1e05d023c
> block number: 2
> block timestamp: 1695026534
> account: 0x0433811BAd4c494f8EeAB84886F2d1d85c80fAdA
> balance: 99.998105632065943366
> gas used: 416594 (0x65b52)
> gas price: 3.270752661 gwei
> value sent: 0 ETH
> total cost: 0.001362575934056634 ETH
> Saving artifacts
-------------------------------------
> Total cost: 0.001894367934056634 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.001894367934056634 ETH
Ganacheで確認
コントラクトのやり取り
- コマンドでコントラクトを操作します
- 以下コマンドでTruffleコンソールに移行
% truffle console
truffle(ganache)>
やり取り用のアカウントを用意
- instance: プロジェクトで作成したMetaCoinアカウント
- accounts: Truffle の組み込みブロックチェーンアカウント
truffle(ganache)> let instance = await MetaCoin.deployed()
undefined
truffle(ganache)> let accounts = await web3.eth.getAccounts()
undefined
プロジェクトで作成したMetaCoinアカウントの残高を確認
- 10000メタコイン、20000ETH
truffle(ganache)> let balance = await instance.getBalance(accounts[0])
undefined
truffle(ganache)> balance.toNumber()
10000
truffle(ganache)> let ether = await instance.getBalanceInEth(accounts[0])
undefined
truffle(ganache)> ether.toNumber()
20000
メタコインの転送
- [プロジェクトで作成したMetaCoinアカウント]から任意の[Truffle の組み込みブロックチェーンアカウント]にメタコイン500を転送
truffle(ganache)> instance.sendCoin(accounts[1], 500)
{
tx: '0xdde7e6cc2c2027514481ffe21de1b029b74aca6f98c4c82d29407c2fa13bc3f2',
receipt: {
transactionHash: '0xdde7e6cc2c2027514481ffe21de1b029b74aca6f98c4c82d29407c2fa13bc3f2',
transactionIndex: 0,
blockNumber: 3,
blockHash: '0x75677027c9729f16ca8cc23ad5e293b9da2e294c5ab7499734fa4dca120f5a8b',
from: '0x0433811bad4c494f8eeab84886f2d1d85c80fada',
to: '0x1e013b453ed87691545f1173ba7d97f1e05d023c',
cumulativeGasUsed: 52297,
gasUsed: 52297,
contractAddress: null,
logs: [ [Object] ],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000010000000008000000400000018000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000200000000000000000002020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: true,
effectiveGasPrice: 3186350418,
type: '0x2',
rawLogs: [ [Object] ]
},
logs: [
{
address: '0x1e013b453Ed87691545F1173ba7D97F1e05d023c',
blockHash: '0x75677027c9729f16ca8cc23ad5e293b9da2e294c5ab7499734fa4dca120f5a8b',
blockNumber: 3,
logIndex: 0,
removed: false,
transactionHash: '0xdde7e6cc2c2027514481ffe21de1b029b74aca6f98c4c82d29407c2fa13bc3f2',
transactionIndex: 0,
id: 'log_796221d5',
event: 'Transfer',
args: [Result]
}
]
}
受取側アカウントの残高を確認
truffle(ganache)> let received = await instance.getBalance(accounts[1])
undefined
truffle(ganache)> received.toNumber()
500
truffle(ganache)> let newBalance = await instance.getBalance(accounts[0])
undefined
truffle(ganache)> newBalance.toNumber()
9500
送信側アカウントの残高を確認
truffle(ganache)> let newBalance = await instance.getBalance(accounts[0])
undefined
truffle(ganache)> newBalance.toNumber()
9500
まとめ
- SolidityはJavaScriptに似ています。難しい言語では無い。
- Truffleのチュートリアルが充実しているので、学習コストが低いイメージです。
- Ganacheで可視化できるので分かりやすい。