お世話になっております。駆け出しエンジニアの松岡です。
約30時間に及ぶ英語のWeb3研修が終わったので、その内容を振り返ります。
自習タイプの研修
本研修は、会社が手取り足取り教えてくれるものではなく、以下の動画を見ながら自習するタイプの研修でした。
https://dappuniversity.teachable.com/courses/enrolled/1818059
個人的には、自分のペースで進められる方が好きなので、非常に良かったです。会社的にも研修にリソースを割く必要が無いし、自立分散を体現している感じで良いのでは。
10万円の研修動画
研修動画の費用は約10万円で、会社持ちでした。あざ。
動画の長さは合計で約30時間、言語は全て英語なので、やりごたえは十分。
研修の内容
- Solidity開発のフレームワークであるHardhatを使って、ERC-20トークンとDEXのスマートコントラクトを開発
- Infuraを使ってGoerliテストネットにデプロイ
- フロントエンドをReact(状態管理はRedux)で開発
- ブロックチェーンとの接続にはEthers.jsとMetamaskを使用
- Fleekを使ってデプロイ
制作物
- Webアプリ
https://long-lake-4026.on.fleek.co/
- ソースコード
https://github.com/kei-matsuoka/blockchain-developer-bootcamp
感想
2週間で終了。正直Nottaで英語を翻訳できたのもでかい。
本研修でWeb3開発の流れを掴めたのが一番の収穫です。
特にHardhatを用いた開発、Ethers.jsでブロックチェーンと接続する方法、スマートコントラクトのデプロイ方法など。
事前にReactを勉強していたので、フロントエンドはサクサク進められて良かったです。
また、いくつかのNodeライブラリを知れたのもありがたい。
ただしこれについては正直理解が甘いので、使いこなすには復習が必要そう。Reduxでの状態管理も理解が甘いかな。
会社での開発は、恐らくフロントエンドをTypeScript + Next.jsで作って、バックエンドはNode + TypeScript + 何かしらのフレームワーク + Prisma、GCPでDBやインフラ、CI/CDパイプラインを作って、Hardhatでスマコン開発 & Alchemyでデプロイとかになりそうな気がしています。
個人的には、サービスの根幹となるバックエンドとスマコン開発に力を入れたいので、その辺りの勉強を優先しつつ、フロントとインフラは最低限(上手いことUIライブラリ、PaaSやIaaSを活用する)で駆け出していきたいです。
以下は個人的なメモ
開発環境
- node 16.14.2
- https://gist.github.com/gwmccubbin/fb80787126657d3274584ffb3d415650
- create-react-appとの差異
プロジェクト作成
- React
## Reactプロジェクトを作成
npx create-react-app blockchain-developer-bootcamp --use-npm
## 依存関係をインストール
npm install
## envファイルを作成
touch .env
- Hardhat
## Hardhatプロジェクトを作成
npx hardhat
- Hardhatでdotenvを有効化
// hardhat.config.js
// Hardhatでdotenvを有効化
require(”dotenv”).config();
// solidityのバージョン指定
// networkにlocalhostを追加
module.exports = {
solidity: "0.8.9",
networks: {
localhost: {}
},
};
開発
- コマンド
## Nodeを立てる
npx hardhat node
## デプロイ
npx hardhat run --network localhost ./scripts/〇〇.js
## コンソールに入る
npx hardhat console --network localhost
## テスト
npx hardhat test
- ブロックチェーンとの疎通確認
## コンソールにて
const token = await ethers.getContractAt("Token", "0x...")
token.address
const accounts = await ethers.getSigners()
accounts[0]
address = accounts[0].address
const balance = await ethers.provider.getBalance(address)
balance.toString()
ethers.utils.formatEther(balance.toString())
デプロイ
- Infuraでスマートコントラクトをデプロイ
- Alchemyでも可
- Goerli FaucetでGoerliテストネットのガス代をもらう
ソースコード
- GitHubリポジトリ
https://github.com/kei-matsuoka/blockchain-developer-bootcamp
- デプロイ
// scripts/〇〇.js
const config = require('../src/config.json')
const tokens = (n) => {
return ethers.utils.parseUnits(n.toString(), 'ether')
}
const wait = (seconds) => {
const milliseconds = seconds * 1000
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
async function main() {
// Fetch accounts from wallet - these are unlocked
const accounts = await ethers.getSigners()
// Fetch network
const { chainId } = await ethers.provider.getNetwork()
console.log("Using chainId:", chainId)
// Fetch deployed tokens
const DApp = await ethers.getContractAt('Token', config[chainId].DApp.address)
console.log(`Dapp Token fetched: ${DApp.address}\n`)
const mETH = await ethers.getContractAt('Token', config[chainId].mETH.address)
console.log(`mETH Token fetched: ${mETH.address}\n`)
const mDAI = await ethers.getContractAt('Token', config[chainId].mDAI.address)
console.log(`mDAI Token fetched: ${mDAI.address}\n`)
// Fetch the deployed exchange
const exchange = await ethers.getContractAt('Exchange', config[chainId].exchange.address)
console.log(`Exchange fetched: ${exchange.address}\n`)
// Give tokens to account[1]
const sender = accounts[0]
const receiver = accounts[1]
let amount = tokens(10000)
// user1 transfers 10,000 mETH...
let transaction, result
transaction = await mETH.connect(sender).transfer(receiver.address, amount)
console.log(`Transferred ${amount} tokens from ${sender.address} to ${receiver.address}\n`)
// Set up exchange users
const user1 = accounts[0]
const user2 = accounts[1]
amount = tokens(10000)
// user1 approves 10,000 Dapp...
transaction = await DApp.connect(user1).approve(exchange.address, amount)
await transaction.wait()
console.log(`Approved ${amount} tokens from ${user1.address}`)
// user1 deposits 10,000 DApp...
transaction = await exchange.connect(user1).depositToken(DApp.address, amount)
await transaction.wait()
console.log(`Deposited ${amount} Ether from ${user1.address}\n`)
// User 2 Approves mETH
transaction = await mETH.connect(user2).approve(exchange.address, amount)
await transaction.wait()
console.log(`Approved ${amount} tokens from ${user2.address}`)
// User 2 Deposits mETH
transaction = await exchange.connect(user2).depositToken(mETH.address, amount)
await transaction.wait()
console.log(`Deposited ${amount} tokens from ${user2.address}\n`)
/////////////////////////////////////////////////////////////
// Seed a Cancelled Order
//
// User 1 makes order to get tokens
let orderId
transaction = await exchange.connect(user1).makeOrder(mETH.address, tokens(100), DApp.address, tokens(5))
result = await transaction.wait()
console.log(`Made order from ${user1.address}`)
// User 1 cancels order
orderId = result.events[0].args.id
transaction = await exchange.connect(user1).cancelOrder(orderId)
result = await transaction.wait()
console.log(`Cancelled order from ${user1.address}\n`)
// Wait 1 second
await wait(1)
/////////////////////////////////////////////////////////////
// Seed Filled Orders
//
// User 1 makes order
transaction = await exchange.connect(user1).makeOrder(mETH.address, tokens(100), DApp.address, tokens(10))
result = await transaction.wait()
console.log(`Made order from ${user1.address}`)
// User 2 fills order
orderId = result.events[0].args.id
transaction = await exchange.connect(user2).fillOrder(orderId)
result = await transaction.wait()
console.log(`Filled order from ${user1.address}\n`)
// Wait 1 second
await wait(1)
// User 1 makes another order
transaction = await exchange.makeOrder(mETH.address, tokens(50), DApp.address, tokens(15))
result = await transaction.wait()
console.log(`Made order from ${user1.address}`)
// User 2 fills another order
orderId = result.events[0].args.id
transaction = await exchange.connect(user2).fillOrder(orderId)
result = await transaction.wait()
console.log(`Filled order from ${user1.address}\n`)
// Wait 1 second
await wait(1)
// User 1 makes final order
transaction = await exchange.connect(user1).makeOrder(mETH.address, tokens(200), DApp.address, tokens(20))
result = await transaction.wait()
console.log(`Made order from ${user1.address}`)
// User 2 fills final order
orderId = result.events[0].args.id
transaction = await exchange.connect(user2).fillOrder(orderId)
result = await transaction.wait()
console.log(`Filled order from ${user1.address}\n`)
// Wait 1 second
await wait(1)
/////////////////////////////////////////////////////////////
// Seed Open Orders
//
// User 1 makes 10 orders
for(let i = 1; i <= 10; i++) {
transaction = await exchange.connect(user1).makeOrder(mETH.address, tokens(10 * i), DApp.address, tokens(10))
result = await transaction.wait()
console.log(`Made order from ${user1.address}`)
// Wait 1 second
await wait(1)
}
// User 2 makes 10 orders
for (let i = 1; i <= 10; i++) {
transaction = await exchange.connect(user2).makeOrder(DApp.address, tokens(10), mETH.address, tokens(10 * i))
result = await transaction.wait()
console.log(`Made order from ${user2.address}`)
// Wait 1 second
await wait(1)
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
- テスト
// test/〇〇.js
const { expect } = require('chai');
const { ethers } = require('hardhat');
const tokens = (n) => {
return ethers.utils.parseUnits(n.toString(), 'ether')
}
describe('Token', () => {
let token, accounts, deployer, receiver, exchange
beforeEach(async () => {
const Token = await ethers.getContractFactory('Token')
token = await Token.deploy('Dapp University', 'DAPP', '1000000')
accounts = await ethers.getSigners()
deployer = accounts[0]
receiver = accounts[1]
exchange = accounts[2]
})
describe('Success', () => {
it('allocates an allowance for delegated token spending', async () => {
expect(await token.allowance(deployer.address, exchange.address)).to.equal(amount)
})
it('emits an Approval event', async () => {
const event = result.events[0]
expect(event.event).to.equal('Approval')
const args = event.args
expect(args.owner).to.equal(deployer.address)
expect(args.spender).to.equal(exchange.address)
expect(args.value).to.equal(amount)
})
})
}
- 設定ファイル
//config.json
{
"31337": {
"exchange": {
"address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"
},
"DApp": {
"address": "0x5FbDB2315678afecb367f032d93F642f64180aa3"
},
"mETH": {
"address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
},
"mDAI": {
"address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"
},
"explorerURL": "#"
},
"42": {
"explorerURL": "https://kovan.etherscan.io/"
}
}
- abi
// abis/〇〇.js
[
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_symbol",
"type": "string"
},
{
"internalType": "uint256",
"name": "_totalSupply",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_to",
"type": "address"
},
{
"internalType": "uint256",
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_from",
"type": "address"
},
{
"internalType": "address",
"name": "_to",
"type": "address"
},
{
"internalType": "uint256",
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
- Interactions
import { ethers } from 'ethers'
import TOKEN_ABI from '../abis/Token.json';
export const loadProvider = (dispatch) => {
const connection = new ethers.providers.Web3Provider(window.ethereum)
dispatch({ type: 'PROVIDER_LOADED', connection })
return connection
}
export const loadNetwork = async (provider, dispatch) => {
const { chainId } = await provider.getNetwork()
dispatch({ type: 'NETWORK_LOADED', chainId })
return chainId
}
export const loadAccount = async (provider, dispatch) => {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
const account = ethers.utils.getAddress(accounts[0])
dispatch({ type: 'ACCOUNT_LOADED', account })
let balance = await provider.getBalance(account)
balance = ethers.utils.formatEther(balance)
dispatch({ type: 'ETHER_BALANCE_LOADED', balance })
return account
}
export const loadTokens = async (provider, addresses, dispatch) => {
let token, symbol
token = new ethers.Contract(addresses[0], TOKEN_ABI, provider)
symbol = await token.symbol()
dispatch({ type: 'TOKEN_1_LOADED', token, symbol })
return token
}