📚
Viemの基礎学習2日目(スマートコントラクトとの対話)
ViemとFoundryを組み合わせたスマートコントラクト開発
日付: 2025年10月26日
学習内容: FoundryとViemを組み合わせたスマートコントラクトのデプロイとインタラクション、関数セレクター、ABIを使用した実践的な開発
1. プロジェクト構造の作成
1.1 プロジェクトの構成
viem/
├── foundry/ # Foundryプロジェクト
│ ├── src/
│ │ └── SomeContract.sol
│ ├── script/
│ │ └── Deploy.s.sol
│ └── foundry.toml
├── src/ # TypeScriptコード
│ ├── deploy.ts
│ ├── contract-interaction.ts
│ ├── function-selector.ts
│ └── with-abi-file.ts
├── package.json
└── tsconfig.json
1.2 プロジェクトの初期化
# Foundryプロジェクトの初期化
forge init foundry --no-git
# Node.jsプロジェクトの初期化
npm init -y
# 必要なパッケージのインストール
npm install viem
npm install --save-dev tsx typescript @types/node
2. スマートコントラクトの作成
2.1 SomeContract.solの作成
Foundryプロジェクト内にシンプルなコントラクトを作成:
// foundry/src/SomeContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;
contract SomeContract {
uint256 public myUint = 10;
function setUint(uint256 _myUint) public {
myUint = _myUint;
}
}
ポイント:
-
myUint: public変数(自動的にgetter関数が生成される) -
setUint(): 値を更新する関数 - 初期値は10に設定
2.2 コンパイル
cd foundry
forge build
cd ..
コンパイル後、foundry/out/SomeContract.sol/SomeContract.jsonが生成されます。
3. デプロイの2つの方法
3.1 方法1: Foundry Scriptを使用
Deploy.s.solの作成
// foundry/script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;
import {Script, console} from "forge-std/Script.sol";
import {SomeContract} from "../src/SomeContract.sol";
contract DeployScript is Script {
function run() external {
vm.startBroadcast();
SomeContract someContract = new SomeContract();
console.log("Contract deployed at:", address(someContract));
vm.stopBroadcast();
}
}
重要な概念
-
external: 外部からのみ呼び出し可能(ガス効率が良い) -
vm.startBroadcast(): トランザクションの収集を開始 -
new SomeContract(): コントラクトをインスタンス化(デプロイ) -
vm.stopBroadcast(): トランザクションの収集を停止
デプロイコマンド
npm run deploy:foundry
# または
forge script foundry/script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--broadcast
--broadcastフラグ:
- なし: シミュレーションのみ(dry run)
- あり: 実際にブロックチェーンに送信
3.2 方法2: TypeScriptを使用
deploy.tsの作成
// src/deploy.ts
import {
createPublicClient,
createWalletClient,
http
} from 'viem'
import { foundry } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import contractJson from '../foundry/out/SomeContract.sol/SomeContract.json'
const publicClient = createPublicClient({
chain: foundry,
transport: http('http://127.0.0.1:8545')
})
const account = privateKeyToAccount(
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
)
const walletClient = createWalletClient({
account,
chain: foundry,
transport: http('http://127.0.0.1:8545')
})
async function deploy() {
console.log('=== Deploy Starting ===')
console.log('Deploying SomeContract...')
// コントラクトをデプロイ
const hash = await walletClient.deployContract({
abi: contractJson.abi,
bytecode: contractJson.bytecode.object as `0x${string}`,
account
})
console.log('Transaction hash:', hash)
// デプロイ完了を待つ
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log('Contract deployed at:', receipt.contractAddress)
console.log('Block number:', receipt.blockNumber.toString())
return receipt.contractAddress
}
deploy().catch(error => {
console.error('実行エラー:', error)
})
重要なポイント
bytecodeの理解:
-
contractJson.bytecode.object: デプロイ用バイトコード(コンストラクタを含む) -
as 0x${string}: TypeScriptの型アサーション(0xプレフィックス付き16進数文字列) - Foundryのコンパイル結果は自動的に0xプレフィックスが付く
なぜ2つの方法があるのか?:
- Foundry Script: Solidityで全て処理、Foundryエコシステムとの統合が容易
- TypeScript: より柔軟、アプリケーションからのデプロイに適している
4. 関数セレクターの理解
4.1 関数セレクターとは
Ethereumで関数を識別する4バイトのハッシュ値。
// src/function-selector.ts
import { keccak256, toBytes } from 'viem'
function getFunctionSelector(signature: string): string {
const bytes = toBytes(signature)
const hash = keccak256(bytes)
return hash.slice(0, 10) // 最初の4バイト(0x + 8文字)
}
console.log('myUint() selector:', getFunctionSelector('myUint()'))
// 出力: 0x06540f7e
console.log('setUint(uint256) selector:', getFunctionSelector('setUint(uint256)'))
// 出力: 0x4ef65c3b
4.2 処理の流れ
-
関数シグネチャの文字列をバイト配列に変換:
toBytes() -
Keccak256でハッシュ化:
keccak256() -
最初の4バイトを取得:
slice(0, 10)(0x + 8文字 = 10文字)
4.3 使用例
// 低レベルAPIでコントラクトを呼び出す
const data = await publicClient.call({
to: contractAddress,
data: '0x06540f7e' // myUint()のfunction selector
})
5. ABIを使用した実践的な開発
5.1 with-abi-file.tsの作成
Foundryのコンパイル結果(JSON)からABIを読み込んで使用:
// src/with-abi-file.ts
import {
createPublicClient,
createWalletClient,
http,
getContract
} from 'viem'
import { foundry } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import type { Address } from 'viem'
import contractJson from '../foundry/out/SomeContract.sol/SomeContract.json'
const contractAddress: Address = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
const publicClient = createPublicClient({
chain: foundry,
transport: http('http://127.0.0.1:8545')
})
const account = privateKeyToAccount(
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
)
const walletClient = createWalletClient({
account,
chain: foundry,
transport: http('http://127.0.0.1:8545')
})
async function main() {
// FoundryのJSON出力からABIを取得
const contract = getContract({
address: contractAddress,
abi: contractJson.abi,
client: { public: publicClient, wallet: walletClient }
})
// 読み取り
console.log('Reading myUint...')
const value = await contract.read.myUint() as bigint
console.log('Current value:', value.toString())
// 書き込み
console.log('\nUpdating to 100...')
const hash = await contract.write.setUint([100n])
await publicClient.waitForTransactionReceipt({ hash })
// 確認
const newValue = await contract.read.myUint() as bigint
console.log('New value:', newValue.toString())
}
main().catch(console.error)
5.2 getContract()の重要性
const contract = getContract({
address: contractAddress,
abi: contractJson.abi,
client: { public: publicClient, wallet: walletClient }
})
この関数が提供するもの:
-
contract.read.*: 型安全な読み取り関数 -
contract.write.*: 型安全な書き込み関数 - 自動補完: IDEが関数名を提案
- 型チェック: コンパイル時にエラーを検出
5.3 BigIntの重要性
const hash = await contract.write.setUint([100n])
// ^
// このnが重要
なぜ100nなのか?:
-
100: JavaScriptの通常の数値(number型) -
100n: BigInt型(任意精度の整数) - Solidityの
uint256は最大2^256-1の値を扱える - JavaScriptの通常の数値では扱えないため、BigIntが必要
実際の違い:
typeof 100 // "number"
typeof 100n // "bigint"
6. 開発フローの完成
6.1 完全な開発サイクル
# 1. Anvilの起動
anvil
# 2. コントラクトを編集
# foundry/src/SomeContract.sol を編集
# 3. コンパイル
cd foundry && forge build && cd ..
# 4. デプロイ
npx tsx src/deploy.ts
# 5. 対話(読み取り、書き込み)
npx tsx src/contract-interaction.ts
6.2 package.jsonのスクリプト
{
"scripts": {
"compile": "cd foundry && forge build && cd ..",
"deploy:foundry": "cd foundry && forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast && cd ..",
"deploy:ts": "tsx src/deploy.ts",
"interact": "tsx src/contract-interaction.ts",
"selector": "tsx src/function-selector.ts",
"with-abi": "tsx src/with-abi-file.ts"
}
}
7. 重要な概念の詳細
7.1 デプロイスクリプトの理解
vm.startBroadcast() と vm.stopBroadcast():
-
startBroadcast(): トランザクションを収集開始 -
stopBroadcast(): トランザクション収集を停止 -
--broadcastフラグ: 収集したトランザクションを実際に送信
ブロードキャストの仕組み:
1. vm.startBroadcast() で収集モード開始
2. new SomeContract() でデプロイトランザクションを収集
3. vm.stopBroadcast() で収集終了
4. --broadcast フラグでチェーンに送信
7.2 bytecodeの種類
2種類のbytecode:
-
bytecode.object: デプロイ用(コンストラクタコードを含む、長い) -
deployedBytecode.object: デプロイ後(コントラクトコードのみ、短い)
なぜ2種類あるのか?:
- デプロイ時: コンストラクタが実行され、初期化コードが破棄される
- デプロイ後: 実装コードのみがブロックチェーンに保存される
7.3 0xプレフィックス
なぜ必要?:
- Ethereumの規格で、アドレス、bytecode、ハッシュは16進数で表記
-
0xは16進数であることを示す - TypeScriptの型安全性(
0x${string})
8. 実行例
8.1 function-selector.tsの実行
npx tsx src/function-selector.ts
出力:
myUint() selector: 0x06540f7e
setUint(uint256) selector: 0x4ef65c3b
8.2 with-abi-file.tsの実行
npx tsx src/with-abi-file.ts
出力:
Reading myUint...
Current value: 100
Updating to 100...
New value: 100
9. 学習のポイント
9.1 習得した技術
- FoundryとViemの統合
- 2種類のデプロイ方法
- 関数セレクターの計算と理解
- ABIの効果的な活用
- BigInt型の理解と使用
9.2 重要な理解事項
- Broadcast機能: シミュレーションと実際の送信の違い
- bytecodeの種類: デプロイ用とデプロイ後の違い
- 型システム: TypeScriptとBigIntの重要性
- ABIの利点: 型安全性と開発効率の向上
9.3 実践的なスキル
- スマートコントラクトのデプロイ
- コントラクトとの対話(読み取り・書き込み)
- 低レベルAPIと高レベルAPIの使い分け
- 関数セレクターの手動計算
10. まとめ
2025年10月26日は、FoundryとViemを組み合わせたスマートコントラクト開発の実践的な学習を行いました。デプロイの2つの方法、関数セレクター、ABIの活用など、本格的なブロックチェーン開発に必要な知識とスキルを習得しました。
10.1 重要な成果
- 完全な開発サイクルの確立
- デプロイ方法の理解と実践
- 低レベルAPIと高レベルAPIの使い分け
- 型安全性の確保
10.2 今後の学習方向
- より複雑なスマートコントラクトの作成
- イベントの監視と処理
- マルチシグネチャとウォレット統合
- ガス最適化の実践
Discussion