🙆

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 重要な注意点

  1. トランザクションの不可逆性: 一度送信したトランザクションは取り消せない
  2. ガス代の確認: 常にガス代を確認し、残高が十分かチェック
  3. エラーハンドリング: 必ずtry-catch文でエラーを捕捉
  4. シミュレーションの活用: simulateContract()で事前にエラーをチェック

7. 学習のポイント

7.1 習得した技術

  1. 読み取り操作の多様な方法

    • 基本的な読み取り
    • 低レベルAPI
    • 複数回の連続読み取り
    • 定期的な監視
    • ブロック情報付き
    • エラーハンドリング
    • 値のフォーマット
  2. 書き込み操作の多様な方法

    • 基本的な書き込み
    • シミュレーション付き書き込み
    • 複数のトランザクション連続実行
    • ガス見積もり
    • エラーハンドリング
    • トランザクション追跡
  3. 実践的な使用例

    • ERC-20トークン操作
    • NFTマーケットプレイス
    • DeFiレンディング
    • DEX取引

7.2 重要な理解事項

  1. 読み取りと書き込みの違い

    • 読み取り: Public Client、ガス代不要
    • 書き込み: Wallet Client、ガス代必要、署名必要
  2. シミュレーションの重要性

    • ガス代を節約
    • エラーを事前に検出
    • ユーザー体験の向上
  3. イベント監視の活用

    • リアルタイム更新
    • 効率的なデータ取得
    • ユーザー通知

7.3 実践的なスキル

  1. エラーハンドリング
  2. ガス最適化
  3. トランザクション追跡
  4. イベント監視

8. まとめ

2025年10月27日は、Viemを使用したスマートコントラクトの読み取り・書き込み操作の多様なパターンと実践的な使用例を学習しました。読み取りと書き込みの基本的な操作から、シミュレーション、ガス見積もり、エラーハンドリング、イベント監視まで、包括的な理解を得ることができました。

8.1 重要な成果

  • 多様な読み取り方法の習得
  • 安全な書き込み方法の習得
  • エラーハンドリングの実践
  • 実践的なアプリケーション例の理解

8.2 今後の学習方向

  • より複雑なコントラクトとの対話
  • マルチシグネチャとウォレット統合
  • オフチェーン同期とキャッシング
  • セキュリティの最適化

Discussion