🙆

Viemの基礎学習4日目(ブラウザからの対話(JavaScript))

に公開

ブラウザからスマートコントラクトとインタラクション

日付: 2025年10月28日
学習内容: ブラウザの開発者コンソールからViemを使用してスマートコントラクトとインタラクションする方法、forge createとforge scriptの違い、ESMモジュールを使用したHTMLファイルでのコントラクト操作

1. forge createとforge scriptの違い

1.1 forge create(コントラクトのデプロイ専用)

コントラクトをデプロイする際に使用するシンプルなコマンド:

forge create src/SomeContract.sol:SomeContract \
  --rpc-url http://localhost:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

特徴:

  • --broadcast は不要(デフォルトで実際にトランザクションを送信)
  • シンプルで直接的なデプロイに使用
  • 1つのコントラクトをデプロイするだけ
  • デプロイ専用のコマンド

実行時の注意点:

  • 実行ディレクトリは foundry/ 内であること
  • パスは src/SomeContract.sol:SomeContract の形式
  • --broadcast は不要だが、実際にネットワークに送信される

1.2 forge script(スクリプト実行)

より柔軟で複雑な操作を行うためのコマンド:

forge script script/Deploy.s.sol \
  --rpc-url http://localhost:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
  --broadcast  # ← これが必要!

特徴:

  • --broadcast が必要(必須ではないが重要)
  • --broadcast なし = シミュレーションのみ(dry run)
  • --broadcast あり = 実際にトランザクション送信
  • 複雑なデプロイやインタラクションに使用
  • デプロイスクリプトを使った運用に適している

1.3 使い分けの指針

項目 forge create forge script
用途 シンプルなデプロイ 複雑な操作やデプロイ
broadcast 不要 必要
デプロイ 可能 可能
複雑な操作 不可 可能
スクリプト 不要 必要

選択の目安:

  • 単にコントラクトを1つデプロイしたい → forge create
  • 複数のコントラクトをデプロイ、初期化、セットアップが必要 → forge script

2. ブラウザからコントラクトとインタラクション

2.1 環境構築

2.1.1 Anvilの起動

anvil

確認事項:

  • Chain ID: 31337
  • Listening on 127.0.0.1:8545
  • デフォルトアカウントが10個作成される(各10000 ETH)

2.1.2 コントラクトのデプロイ

cd foundry
forge create src/SomeContract.sol:SomeContract \
  --rpc-url http://localhost:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

デプロイ結果の例:

  • コントラクトアドレス: 0x5FbDB2315678afecb367f032d93F642f64180aa3
  • デプロイアカウント: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

2.1.3 HTTPサーバーの起動

python3 -m http.server 8000

2.2 HTMLファイルの作成

2.2.1 基本的な構造

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <title>Viem Contract Interaction</title>
</head>
<body>
    <h1>Viem + Foundry Demo</h1>
    <script type="module">
        // Viemのコードをここに記述
    </script>
</body>
</html>

2.2.2 チェーン設定の重要性

注意点: AnvilのChain IDは 31337 だが、viemのデフォルト localhost chainは 1337 を使用する。

正しいチェーン設定:

import { createPublicClient, createWalletClient, http, parseAbi, defineChain } from 'https://esm.sh/viem@2.x'
import { privateKeyToAccount } from 'https://esm.sh/viem@2.x/accounts'

// Anvilのチェーン設定(chain ID: 31337)
const anvil = defineChain({
    id: 31337,  // ← これが重要!
    name: 'Anvil Localhost',
    nativeCurrency: {
        decimals: 18,
        name: 'Ether',
        symbol: 'ETH',
    },
    rpcUrls: {
        default: {
            http: ['http://127.0.0.1:8545'],
        },
    },
})

エラー例:

invalid chain id for signer

このエラーは、Chain IDが一致していない場合に発生する。

2.2.3 クライアントの作成

// コントラクト設定
const CONTRACT_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'

const contractABI = parseAbi([
    'function myUint() view returns (uint256)',
    'function setUint(uint256 _myUint) public'
])

// アカウント設定
const account = privateKeyToAccount(
    '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
)

// Public Client(読み取り専用)
const publicClient = createPublicClient({
    chain: anvil,  // ← カスタムチェーンを使用
    transport: http('http://localhost:8545')
})

// Wallet Client(書き込み用)
const walletClient = createWalletClient({
    account,
    chain: anvil,  // ← カスタムチェーンを使用
    transport: http('http://localhost:8545')
})

2.3 関数の実装

2.3.1 読み取り関数

// myUintを読み取る関数
window.readMyUint = async () => {
    try {
        const result = await publicClient.readContract({
            address: CONTRACT_ADDRESS,
            abi: contractABI,
            functionName: 'myUint'
        })
        console.log('myUint value:', result.toString())
        return result
    } catch (error) {
        console.error('Error reading myUint:', error)
    }
}

ポイント:

  • window オブジェクトに関数を割り当てることで、ブラウザコンソールから呼び出し可能に
  • 戻り値は bigint なので、toString() で文字列に変換
  • エラーハンドリングを実装

2.3.2 書き込み関数

// myUintを更新する関数
window.updateMyUint = async (newValue) => {
    try {
        console.log(`Updating myUint to ${newValue}...`)
        
        // トランザクション送信
        const hash = await walletClient.writeContract({
            address: CONTRACT_ADDRESS,
            abi: contractABI,
            functionName: 'setUint',
            args: [BigInt(newValue)]
        })
        
        console.log('Transaction hash:', hash)
        
        // トランザクションの確認を待つ
        const receipt = await publicClient.waitForTransactionReceipt({ hash })
        console.log('Transaction confirmed!', receipt)
        
        // 更新された値を読み取る
        const updatedValue = await window.readMyUint()
        return updatedValue
    } catch (error) {
        console.error('Error updating myUint:', error)
    }
}

ポイント:

  • writeContract でトランザクションを送信
  • waitForTransactionReceipt でトランザクションの確認を待つ
  • 引数は BigInt に変換
  • トランザクション後、自動的に更新された値を読み取る

2.3.3 アカウント情報取得関数

// アカウント情報を取得
window.getAccounts = async () => {
    try {
        const accounts = await publicClient.request({ method: 'eth_accounts' })
        console.log('Available accounts:', accounts)
        console.log('Using account:', account.address)
        return accounts
    } catch (error) {
        console.error('Error getting accounts:', error)
        console.log('Using account:', account.address)
    }
}

注意点:

  • publicClient.getAddresses() は存在しない
  • publicClient.request({ method: 'eth_accounts' }) を使用する
  • エラーハンドリングを実装

2.4 ブラウザでの使用方法

2.4.1 ページを開く

http://localhost:8000/index.html

2.4.2 開発者コンソールを開く

  • Windows: F12 または Ctrl + Shift + I
  • Mac: Cmd + Option + I

2.4.3 コマンドの実行

// 1. 値を読み取る
await readMyUint()

// 出力例:
// myUint value: 10n

// 2. 値を更新する
await updateMyUint(50)

// 出力例:
// Updating myUint to 50...
// Transaction hash: 0xbbc48cd71d5367f48512d30de2cbaaded637dc610ba5f258afede69619e395c5
// Transaction confirmed! { ... }
// myUint value: 50n

// 3. 再度読み取って確認
await readMyUint()

// 出力例:
// myUint value: 50n

// 4. アカウント情報を取得
await getAccounts()

2.5 ESMモジュールの使用

<script type="module">
    import { createPublicClient, createWalletClient, http, parseAbi, defineChain } from 'https://esm.sh/viem@2.x'
    import { privateKeyToAccount } from 'https://esm.sh/viem@2.x/accounts'
    
    // コード...
</script>

ポイント:

  • type="module" でES6モジュールを有効化
  • CDNから直接インポート可能(https://esm.sh/viem@2.x
  • ローカルにインストール不要
  • 最新版が自動的に読み込まれる

3. トラブルシューティング

3.1 よくあるエラーと解決方法

エラー1: invalid chain id for signer

原因: Chain IDが一致していない

解決方法:

// カスタムチェーンを定義
const anvil = defineChain({
    id: 31337,  // AnvilのChain ID
    // ...
})

エラー2: publicClient.getAddresses is not a function

原因: getAddresses() メソッドが存在しない

解決方法:

// 正しい方法
const accounts = await publicClient.request({ method: 'eth_accounts' })

エラー3: Connection refused

原因: Anvilが起動していない

解決方法:

anvil

エラー4: ファイルが見つからない

原因: forge create を間違ったディレクトリで実行

解決方法:

cd foundry
forge create src/SomeContract.sol:SomeContract --rpc-url http://localhost:8545 --private-key ...

3.2 デバッグのコツ

  1. ブラウザコンソールを確認: エラーメッセージを確認
  2. Anvilのログを確認: トランザクションが到達しているか確認
  3. Networkタブを確認: HTTPリクエストが正しく送信されているか確認
  4. チェーンIDを確認: 31337 が正しく設定されているか確認

4. まとめ

4.1 重要なポイント

  1. forge create vs forge script:

    • forge create: シンプルなデプロイに使用、--broadcast 不要
    • forge script: 複雑な操作に使用、--broadcast 必要
  2. チェーン設定:

    • AnvilのChain IDは 31337
    • defineChain でカスタムチェーンを定義する必要がある
  3. ブラウザからの操作:

    • window オブジェクトに関数を割り当てる
    • ESMモジュールを使用(type="module"
    • publicClient.request を使用してRPCメソッドを呼び出し
  4. 実用的な開発フロー:

    1. Anvilを起動
    2. コントラクトをデプロイ
    3. HTTPサーバーを起動
    4. ブラウザでページを開く
    5. 開発者コンソールでコマンドを実行

4.2 学習成果

  • ブラウザからスマートコントラクトとインタラクションする方法を習得
  • forge createとforge scriptの使い分けを理解
  • ESMモジュールを使用したモダンなJavaScriptの記述方法を習得
  • エラーハンドリングとデバッグの技術を習得

4.3 今後の展開

  • より複雑なコントラクト操作
  • UI要素を追加したブラウザアプリケーション
  • 複数のコントラクト間でのインタラクション
  • イベント監視の実装

Discussion