🙆
Viemの基礎学習3日目(スマートコントラクトとの対話とその応用)
スマートコントラクトの読み取り・書き込み操作の実践
日付: 2025年10月27日
学習内容: Viemを使用したスマートコントラクトの読み取り・書き込み操作の多様なパターン、実践的な使用例、エラーハンドリング、イベント監視
1. 読み取り操作の詳細
1.1 基本的な読み取り
コントラクトから値を読み取る最も単純な方法:
// 方法1: readContract を使った基本的な読み取り
const value = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'myUint'
})
console.log('myUint の値:', value.toString())
console.log('型:', typeof value) // bigint
ポイント:
-
publicClient: 読み取り専用クライアント - 戻り値は常に
bigint型 -
toString()で文字列に変換
1.2 低レベルAPIを使用
関数セレクターを直接使用した読み取り:
// 方法2: 低レベルAPI(eth_call)
// myUint()のfunction selector: 0x06540f7e
const data = await publicClient.call({
to: CONTRACT_ADDRESS,
data: '0x06540f7e'
})
console.log('生データ:', data.data)
// 16進数をBigIntに変換
if (data.data) {
const value = BigInt(data.data)
console.log('デコード後:', value.toString())
}
いつ使うべきか?:
- ABIがない場合
- カスタムエンコードが必要な場合
- デバッグ目的
1.3 複数回の連続読み取り
// 方法3: 複数回読み取り
const values: bigint[] = []
for (let i = 0; i < 5; i++) {
const value = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'myUint'
})
values.push(value)
console.log(`読み取り ${i + 1}: ${value}`)
// 少し待機(オプション)
await new Promise(resolve => setTimeout(resolve, 100))
}
console.log('すべての値:', values.map(v => v.toString()))
1.4 定期的な監視(ポーリング)
// 方法4: 定期的に値を監視
async function monitorValue(intervalMs: number = 5000, maxIterations: number = 3) {
let previousValue: bigint | null = null
let iteration = 0
const intervalId = setInterval(async () => {
const currentValue = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'myUint'
})
const timestamp = new Date().toLocaleTimeString()
console.log(`[${timestamp}] 現在の値: ${currentValue}`)
// 値が変わったか確認
if (previousValue !== null && currentValue !== previousValue) {
console.log(`⚠️ 値が変更されました: ${previousValue} → ${currentValue}`)
}
previousValue = currentValue
iteration++
if (iteration >= maxIterations) {
clearInterval(intervalId)
}
}, intervalMs)
}
使用例:
- 価格監視
- オークションの進行状況監視
- ガバナンス投票の状況確認
1.5 ブロック情報付き読み取り
// 方法5: ブロック情報付き読み取り
const blockNumber = await publicClient.getBlockNumber()
console.log('現在のブロック番号:', blockNumber)
const value = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'myUint'
})
console.log(`ブロック ${blockNumber} 時点の値: ${value}`)
// ブロックの詳細情報
const block = await publicClient.getBlock({ blockNumber })
console.log('ブロックタイムスタンプ:', new Date(Number(block.timestamp) * 1000).toLocaleString())
1.6 エラーハンドリング
// 方法6: エラーハンドリング付き
try {
const value = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'myUint'
})
console.log('✅ 読み取り成功:', value.toString())
return { success: true, value }
} catch (error: any) {
console.error('❌ 読み取り失敗')
if (error.message.includes('could not detect network')) {
console.error(' 原因: Anvilが起動していません')
} else if (error.message.includes('contract')) {
console.error(' 原因: コントラクトアドレスが正しくありません')
}
return { success: false, error: error.message }
}
1.7 値のフォーマット
// 方法7: 値のフォーマット例
const value = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'myUint'
})
console.log('生の値(BigInt):', value)
console.log('文字列:', value.toString())
console.log('数値:', Number(value))
console.log('16進数:', '0x' + value.toString(16))
console.log('2進数:', value.toString(2))
2. 書き込み操作の詳細
2.1 基本的な書き込み
// 方法1: 基本的な書き込み
// 書き込み前の値を確認
const beforeValue = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'myUint'
})
console.log('書き込み前の値:', beforeValue.toString())
// 値を更新
const newValue = 50n
const hash = await walletClient.writeContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'setUint',
args: [newValue]
})
console.log('トランザクションハッシュ:', hash)
// 確認を待つ
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log('ステータス:', receipt.status) // 'success' or 'reverted'
console.log('ブロック番号:', receipt.blockNumber.toString())
console.log('使用ガス:', receipt.gasUsed.toString())
// 書き込み後の値を確認
const afterValue = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'myUint'
})
console.log('書き込み後の値:', afterValue.toString())
2.2 シミュレーション→書き込み(推奨)
// 方法2: シミュレーション→書き込み
const newValue = 100n
// まずシミュレート(ガス代なし、エラーチェック)
console.log('トランザクションをシミュレート中...')
const { request } = await publicClient.simulateContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'setUint',
args: [newValue],
account
})
console.log('✅ シミュレーション成功(エラーなし)')
// シミュレーション成功後に実行
console.log('トランザクション送信中...')
const hash = await walletClient.writeContract(request)
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log('✅ トランザクション確認完了')
シミュレーションの利点:
- ガス代がかからない
- エラーを事前に検出
- ガス見積もりを取得可能
2.3 複数のトランザクションを連続実行
// 方法3: 複数トランザクション
const values = [10n, 20n, 30n, 40n, 50n]
for (let i = 0; i < values.length; i++) {
const value = values[i]
console.log(`[${i + 1}/${values.length}] 値を ${value} に更新中...`)
const hash = await walletClient.writeContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'setUint',
args: [value]
})
console.log(' TX:', hash.slice(0, 10) + '...')
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log(' ✅ 完了 (Gas:', receipt.gasUsed.toString() + ')')
}
// 最終値を確認
const finalValue = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'myUint'
})
console.log('最終値:', finalValue.toString())
2.4 ガス見積もり
// 方法4: ガス見積もり
const newValue = 200n
// ガス見積もり
const gasEstimate = await publicClient.estimateContractGas({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'setUint',
args: [newValue],
account
})
console.log('推定ガス量:', gasEstimate.toString())
// ガス価格を取得
const gasPrice = await publicClient.getGasPrice()
console.log('ガス価格:', gasPrice.toString(), 'wei')
// 推定コスト(wei)
const estimatedCost = gasEstimate * gasPrice
console.log('推定コスト:', estimatedCost.toString(), 'wei')
console.log('推定コスト:', Number(estimatedCost) / 1e18, 'ETH')
// トランザクション実行
const hash = await walletClient.writeContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'setUint',
args: [newValue],
gas: gasEstimate
})
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log('実際のガス使用量:', receipt.gasUsed.toString())
console.log('実際のコスト:', Number(receipt.gasUsed * gasPrice) / 1e18, 'ETH')
ガス見積もりの重要性:
- ユーザーにコストを表示
- 残高不足を事前にチェック
- 最適なガス価格の設定
2.5 エラーハンドリング
// 方法5: エラーハンドリング
try {
// シミュレート
await publicClient.simulateContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'setUint',
args: [newValue],
account
})
// 書き込み
const hash = await walletClient.writeContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'setUint',
args: [newValue]
})
// 確認(タイムアウト付き)
const receipt = await publicClient.waitForTransactionReceipt({
hash,
timeout: 60_000
})
if (receipt.status === 'success') {
console.log('✅ トランザクション成功')
} else {
console.log('❌ トランザクション失敗(revert)')
}
} catch (error: any) {
console.error('❌ エラー発生')
if (error.message.includes('insufficient funds')) {
console.error(' 原因: 残高不足')
} else if (error.message.includes('nonce')) {
console.error(' 原因: Nonce エラー')
} else if (error.message.includes('gas')) {
console.error(' 原因: ガス不足')
} else if (error.message.includes('revert')) {
console.error(' 原因: コントラクトがトランザクションを拒否')
}
}
主要なエラータイプ:
- insufficient funds: 残高不足
- nonce: nonceエラー
- gas: ガス不足
- revert: コントラクト側での拒否
2.6 トランザクションの追跡
// 方法6: トランザクション追跡
console.log('📤 トランザクション送信前')
console.log(' 送信元:', account.address)
console.log(' コントラクト:', CONTRACT_ADDRESS)
const balanceBefore = await publicClient.getBalance({
address: account.address
})
console.log(' 残高:', Number(balanceBefore) / 1e18, 'ETH')
// 送信
const hash = await walletClient.writeContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'setUint',
args: [newValue]
})
console.log('⏳ トランザクション送信完了')
console.log(' Hash:', hash)
// 確認
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log('✅ トランザクション確認完了')
console.log(' ステータス:', receipt.status)
console.log(' ブロック:', receipt.blockNumber.toString())
console.log(' ガス使用:', receipt.gasUsed.toString())
const balanceAfter = await publicClient.getBalance({
address: account.address
})
const cost = balanceBefore - balanceAfter
console.log(' コスト:', Number(cost) / 1e18, 'ETH')
// トランザクション詳細を取得
const transaction = await publicClient.getTransaction({ hash })
console.log('📋 トランザクション詳細:')
console.log(' From:', transaction.from)
console.log(' To:', transaction.to)
console.log(' Value:', transaction.value.toString(), 'wei')
console.log(' Gas:', transaction.gas.toString())
3. 実践的な使用例
3.1 読み取り操作の例(ERC20トークン)
// 残高確認
const myBalance = await contract.read.balanceOf(['0xMyAddress'])
console.log(`残高: ${formatEther(myBalance)} tokens`)
// 総供給量
const total = await contract.read.totalSupply()
console.log(`総供給量: ${formatEther(total)} tokens`)
// 名前とシンボル
const name = await contract.read.name()
const symbol = await contract.read.symbol()
console.log(`${name} (${symbol})`)
3.2 書き込み操作の例(ERC-20トークンの送信)
// 10トークンを送信
const hash = await contract.write.transfer([
'0xRecipientAddress',
parseEther('10')
])
console.log('Transaction hash:', hash)
// トランザクション確認を待つ
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log('Status:', receipt.status) // 'success' or 'reverted'
3.3 ETHと一緒に送信
// NFTを購入(0.1 ETH支払い)
await contract.write.buyNFT([tokenId], {
value: parseEther('0.1')
})
// ステーキング(1 ETH預ける)
await contract.write.stake([], {
value: parseEther('1')
})
重要なポイント:
-
valueオプションでETHを送信 -
parseEther()でETHをWeiに変換
3.4 イベントの監視
過去のイベント取得
const logs = await publicClient.getContractEvents({
address: contractAddress,
abi: contractABI,
eventName: 'Transfer',
fromBlock: 1000000n,
toBlock: 'latest'
})
console.log('過去の送金履歴:', logs)
リアルタイム監視
const unwatch = publicClient.watchContractEvent({
address: contractAddress,
abi: contractABI,
eventName: 'Transfer',
onLogs: (logs) => {
logs.forEach(log => {
console.log(`送金: ${log.args.from} → ${log.args.to}`)
console.log(`金額: ${formatEther(log.args.value)}`)
})
}
})
// 監視停止
// unwatch()
4. 実践的なアプリケーション例
4.1 DEXでの取引例
// 1. 現在の価格を確認(読み取り)
const price = await dexContract.read.getPrice(['USDC', 'ETH'])
console.log(`1 ETH = ${price} USDC`)
// 2. 自分の残高を確認(読み取り)
const usdcBalance = await usdcContract.read.balanceOf([myAddress])
console.log(`USDC残高: ${formatUnits(usdcBalance, 6)}`)
// 3. DEXにUSDCの使用許可を与える(書き込み)
const approveHash = await usdcContract.write.approve([
dexAddress,
parseUnits('1000', 6) // 1000 USDC
])
await publicClient.waitForTransactionReceipt({ hash: approveHash })
// 4. スワップを実行(書き込み)
const swapHash = await dexContract.write.swap([
'USDC',
'ETH',
parseUnits('1000', 6), // 1000 USDC
parseEther('0.4') // 最低0.4 ETH受取
])
await publicClient.waitForTransactionReceipt({ hash: swapHash })
// 5. 結果を確認(読み取り)
const newBalance = await publicClient.getBalance({ address: myAddress })
console.log(`新しいETH残高: ${formatEther(newBalance)}`)
4.2 NFTマーケットプレイス
// 1. NFTが販売中か確認(読み取り)
const listing = await marketContract.read.getListing([tokenId])
console.log(`価格: ${formatEther(listing.price)} ETH`)
console.log(`販売者: ${listing.seller}`)
// 2. 自分のETH残高を確認(読み取り)
const balance = await publicClient.getBalance({ address: myAddress })
if (balance < listing.price) {
console.log('残高不足')
return
}
// 3. NFTを購入(書き込み + ETH送信)
const hash = await marketContract.write.buyNFT([tokenId], {
value: listing.price
})
await publicClient.waitForTransactionReceipt({ hash })
// 4. NFTの所有権を確認(読み取り)
const owner = await nftContract.read.ownerOf([tokenId])
console.log(`新しいオーナー: ${owner}`)
console.log(`購入成功: ${owner === myAddress}`)
4.3 DeFiレンディング
// 1. 現在のAPY(年利)を確認(読み取り)
const apy = await lendingContract.read.getSupplyAPY()
console.log(`供給APY: ${apy / 100}%`)
// 2. 自分の預金額を確認(読み取り)
const deposited = await lendingContract.read.balanceOf([myAddress])
console.log(`預金: ${formatEther(deposited)} ETH`)
// 3. 追加で預金(書き込み + ETH送信)
const depositHash = await lendingContract.write.deposit([], {
value: parseEther('5') // 5 ETH預ける
})
await publicClient.waitForTransactionReceipt({ hash: depositHash })
// 4. 利息を確認(読み取り)
const interest = await lendingContract.read.getAccruedInterest([myAddress])
console.log(`獲得利息: ${formatEther(interest)} ETH`)
// 5. 引き出し(書き込み)
const withdrawHash = await lendingContract.write.withdraw([
parseEther('2') // 2 ETH引き出す
])
await publicClient.waitForTransactionReceipt({ hash: withdrawHash })
5. できることのまとめ
5.1 データ取得
✅ 残高、価格、数量の確認
✅ 所有権の確認
✅ 利率、手数料の計算
✅ 状態の確認(承認済みか、有効か等)
5.2 資産操作
✅ トークンの送信・受信
✅ NFTの転送・ミント
✅ トークンの交換(スワップ)
✅ ステーキング・アンステーキング
5.3 承認・許可
✅ トークン使用許可(Approval)
✅ オペレーター設定
✅ 権限の委譲
5.4 金融操作
✅ 預金・引き出し
✅ 借入・返済
✅ 報酬の請求
✅ 手数料の支払い
5.5 イベント追跡
✅ 過去のトランザクション履歴
✅ リアルタイム通知
✅ 価格変動の監視
✅ 活動ログの取得
6. 制限事項と注意点
6.1 できないこと
❌ プライベート変数の直接読み取り
❌ 他人のウォレットからの送信(秘密鍵が必要)
❌ トランザクションの取り消し(一度送信したら不可逆)
❌ ガス代なしでの状態変更
6.2 重要な注意点
- トランザクションの不可逆性: 一度送信したトランザクションは取り消せない
- ガス代の確認: 常にガス代を確認し、残高が十分かチェック
- エラーハンドリング: 必ずtry-catch文でエラーを捕捉
-
シミュレーションの活用:
simulateContract()で事前にエラーをチェック
7. 学習のポイント
7.1 習得した技術
-
読み取り操作の多様な方法
- 基本的な読み取り
- 低レベルAPI
- 複数回の連続読み取り
- 定期的な監視
- ブロック情報付き
- エラーハンドリング
- 値のフォーマット
-
書き込み操作の多様な方法
- 基本的な書き込み
- シミュレーション付き書き込み
- 複数のトランザクション連続実行
- ガス見積もり
- エラーハンドリング
- トランザクション追跡
-
実践的な使用例
- ERC-20トークン操作
- NFTマーケットプレイス
- DeFiレンディング
- DEX取引
7.2 重要な理解事項
-
読み取りと書き込みの違い
- 読み取り: Public Client、ガス代不要
- 書き込み: Wallet Client、ガス代必要、署名必要
-
シミュレーションの重要性
- ガス代を節約
- エラーを事前に検出
- ユーザー体験の向上
-
イベント監視の活用
- リアルタイム更新
- 効率的なデータ取得
- ユーザー通知
7.3 実践的なスキル
- エラーハンドリング
- ガス最適化
- トランザクション追跡
- イベント監視
8. まとめ
2025年10月27日は、Viemを使用したスマートコントラクトの読み取り・書き込み操作の多様なパターンと実践的な使用例を学習しました。読み取りと書き込みの基本的な操作から、シミュレーション、ガス見積もり、エラーハンドリング、イベント監視まで、包括的な理解を得ることができました。
8.1 重要な成果
- 多様な読み取り方法の習得
- 安全な書き込み方法の習得
- エラーハンドリングの実践
- 実践的なアプリケーション例の理解
8.2 今後の学習方向
- より複雑なコントラクトとの対話
- マルチシグネチャとウォレット統合
- オフチェーン同期とキャッシング
- セキュリティの最適化
Discussion