# Tenderly + Vercel + Foundry + monorepoでのEthereum開発が楽すぎる話 **Published by:** [0xtomo](https://paragraph.com/@0xtomo/) **Published on:** 2025-12-20 **Categories:** tenderly, japanese, ethereum, solidity **URL:** https://paragraph.com/@0xtomo/tenderly-vercel-foundry-monorepo ## Content こんにちは。TanéでCTOをしていますTomo(X: HAIL)です。この記事はWeb3 Advent Calendar 2025 18日目の記事になります。2記事目の寄稿なんですが、1記事目はQiitaに直接書き、今回はこのParagraph(旧Mirrors.xyz)に書いてみます。一体どこがいいやら。 ついこの間まで5年以上にわたり同じプロダクトをメンテしていてそのtipsをまとめたりしたんですが、今久しぶりに完全新規の開発をしていて、ハードルの下がり方がすごいのでシェアします。想定読者はEVMチェーン上(Ethereum, Base, Arbitrum, Polygon, ...)でサービス開発をしている、もしくはしようと思っているエンジニアです。別段超最新のstackでもないので、知っとるわ! という方もいるかとは思いますが、そういう人は連絡ください。採用しますw 目的とStack 果たしたい目的は以下を考えます: EVM上で動作するwebアプリを爆速で開発したい フロントエンドもコントラクトも頻繁に実装が変わる中、それに対応するQA環境を爆速で整備したい EVM上で動作するwebアプリのシステム構成としては、 フロントエンド スマートコントラクト オンチェーンデータをクエリできるAPI(The Graphなど) 必要であればバックエンドやデータベース だと思いますが、この記事では1, 2をメインでカバーし、3は軽く触れます。 Stack: フロントエンド: Next.js ホスティング: Vercel Solidity開発、デプロイ: Foundry CI/CD: GitHub Actions テストネット: Tenderly レポジトリ・パッケージ: monorepo (Turborepoを採用しましたが何でも良い) , pnpm packages 作ったプロセス monorepoをそのままLLMが使えるIDEで開き、爆速開発する GitHubに変更をpushする GitHub Actionが走る。この時点ではVercelのデプロイは行われない コントラクトに変更がある場合は5, ない場合、つまりフロントエンドだけの変更の場合は8に進む 新規のプライベートなテストネットが作成される そのテストネットに必要なコントラクトがデプロイされる デプロイされたコントラクトのアドレスがVercelの環境変数に記述される Vercelのデプロイが走る 変更で入った新機能をテストできるQA環境のできあがり! 主要プロセスの解説 monorepoをそのままIDEで開き、爆速開発する これはなんていうか別にEVM開発に限らず、従来のフロントエンド、バックエンド開発でも同じですが、複数のシステムをまたいでLLMにファイルをメンションし、「このコントラクトの変更をもとにこのUIの挙動をこう変更してくれよな!」と命令するのが圧倒的に楽だと言うだけの話です。僕はIDEはCursor、モデルはGPT 5.2, Gemini 3あたりを使っていますが何を使おうがこの点に関してはほぼ一緒かなと思います。 参考までにファイル構成は以下のようになります。 # 省略しているフォルダやファイルも多くあります .github/ # GitHub Actions workflows/ apps/ web/ # Next.js src/ app/ # Next.js App Router packages/ contracts/ ## Foundryプロジェクトのtop broadcast/ script/ src/ subgraph/ pnpm-workspace.yaml # pnpm packagesの設定ファイル turbo.json # Turborepoの設定ファイル GitHub Actionが走る。この時点でVercelデプロイは行われない コントラクトの変更やそれが動くテストネットのRPC URLなどを反映させたフロントエンドを自動でデプロイしたいので、push時点でのVercelデプロイはignore(キャンセル)させます。VercelはNext.jsアプリ直下に vercel.json を置くとそれを読んでくれます。 # apps/web/vercel.json { "$schema": "https://openapi.vercel.sh/vercel.json", "ignoreCommand": "bash ./scripts/vercel-ignore.sh" } scripts/vercel-ignore.sh にはどういうときにgit pushだけでデプロイが動いてほしいか、ほしくないかを記述しました。例えば、本番環境やステージングは動いてほしいとか、この環境変数が立っているときはしてほしいとか、開発のステージや要件次第。 コントラクトに変更がある場合は5, ない場合、つまりフロントエンドだけの変更の場合は8に進む GitHub Actionの設定でこんな感じで記述しました。まあこんなの要件を伝えればAIが書いてくれるんで、新規で作っても以下をコピペして「こういう風に変えたい」とか言っても数分で終わるでしょう。 # 例えば .github/workflows/qa-main.yml jobs: changes: if: github.event_name == 'workflow_dispatch' || vars.QA_MAIN_ENABLED == 'true' runs-on: ubuntu-latest permissions: contents: read outputs: contracts_changed: ${{ steps.diff.outputs.contracts_changed }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - id: diff run: | set -euo pipefail BEFORE="${{ github.event.before }}" AFTER="${{ github.sha }}" if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then echo "contracts_changed=true" >> "$GITHUB_OUTPUT" exit 0 fi if git diff --name-only "$BEFORE" "$AFTER" | grep -q '^packages/contracts/'; then echo "contracts_changed=true" >> "$GITHUB_OUTPUT" else echo "contracts_changed=false" >> "$GITHUB_OUTPUT" fi これで contracts_changed が true ならコントラクト変更あり、 false ならコントラクト変更無しということでどのステップに進むかを指示するわけですね。 新規のプライベートなテストネットが作成される ここからはTenderlyのVirtual TestNetsというプロダクトを使います。これは自分たち専用のテストネットを、既存のチェーンをforkする形で作れるプロダクトです。例えば「Base mainnetの最新blockをforkしたチェーン作って」みたいな感じです。 作成したチェーン情報例 チェーンごとの機能一覧。なんとなく察せるっしょ TenderlyはGitHub Actionsを用意してくれているのでそれを使います。使い方例は以下、レポジトリはここです。 jobs: qa: steps: - name: Create Tenderly Virtual TestNet if: inputs.run_tenderly || inputs.run_foundry || (inputs.run_vercel && inputs.vercel_update_env) # 走らせる条件。要件次第 uses: Tenderly/vnet-github-action@v1.0.17 with: mode: CD access_key: ${{ secrets.tenderly_access_token }} account_name: ${{ inputs.tenderly_account_name }} project_name: ${{ inputs.tenderly_project_name }} testnet_name: ${{ inputs.testnet_name }} # 特に何も入れないでもユニークな名前が割り当てられます network_id: | ${{ inputs.tenderly_network_id }} chain_id_prefix: ${{ inputs.chain_id_prefix }} state_sync: ${{ inputs.state_sync }} public_explorer: ${{ inputs.public_explorer }} これで自分たち専用の、どこかのチェーンをforkしたテストネットが作成されました。 そのテストネットに必要なコントラクトがデプロイされる まずデプロイにはgasが必要なので、それを送ります。自分たちのテストネットでやりたい放題して大丈夫ですから、なんのトークンでも送れる機能があります。fork元のチェーンにあるトークンならなんでもいけます。下はシンプルにETHを送るときですね。 tenderly_setBalance という命令を使っているのが分かると思います。 jobs: qa: steps: ... - name: Fund deployer (Tenderly unlimited faucet) if: inputs.run_foundry env: ADMIN_RPC_URL: ${{ env.ADMIN_RPC_URL }} DEPLOYER_ADDRESS: ${{ inputs.deployer_address }} DEPLOYER_BALANCE_WEI_HEX: ${{ inputs.deployer_balance_wei_hex }} run: | set -euo pipefail [ -n "${ADMIN_RPC_URL:-}" ] && echo "::add-mask::$ADMIN_RPC_URL" curl -sS "${ADMIN_RPC_URL}" \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "tenderly_setBalance", "params": [["'"${DEPLOYER_ADDRESS}"'"], "'"${DEPLOYER_BALANCE_WEI_HEX}"'"], "id": 1 }' そしたらコントラクトをデプロイしましょう。デプロイするスクリプトは一つにまとめておいたほうが楽かなとは思いますが、複数実行してもいいでしょう。 jobs: qa: steps: ... - name: Install Foundry if: inputs.run_foundry uses: foundry-rs/foundry-toolchain@v1 - name: Deploy contracts with Forge Script if: inputs.run_foundry working-directory: ${{ inputs.foundry_dir }} env: PRIVATE_KEY: ${{ inputs.deployer_private_key }} # 基本使って捨てるQA環境用なので、漏れてもいい前提の鍵を利用するのをおすすめします。漏れちゃいけない鍵の管理は各自考えましょう RPC_URL: ${{ env.RPC_URL }} run: | set -euo pipefail forge script script/${{ inputs.forge_script_file }} \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" \ --broadcast \ -vvv デプロイされたコントラクトのアドレスがVercelの環境変数に記述される forge script コマンドは broadcast/{{Chain ID}}/{{スクリプトファイル名}}/run-latest.jsonにデプロイ結果を記述するので、そこからデプロイされたコントラクトのアドレスを抜き出します。 jobs: qa: steps: ... - name: Extract deployed contract addresses if: inputs.run_foundry env: FOUNDRY_DIR: ${{ inputs.foundry_dir }} CHAIN_ID: ${{ env.CHAIN_ID }} FORGE_SCRIPT_FILE: ${{ inputs.forge_script_file }} run: | set -euo pipefail CONTRACTS_ENV_FILE="/tmp/contracts.env" : > "$CONTRACTS_ENV_FILE" bash ./scripts/ci/extract-foundry-address.sh "${FOUNDRY_DIR}" "${CHAIN_ID}" "${FORGE_SCRIPT_FILE}" "${CONTRACTS_ENV_FILE}" # Export for subsequent steps as environment variables. cat "$CONTRACTS_ENV_FILE" >> "$GITHUB_ENV" echo "Extracted:" cat "$CONTRACTS_ENV_FILE" || true scripts/ci/extract-foundry-address.sh で抜き出しているわけですが、このコードはちょっと長いので割愛します。まあAIに言えば秒で100行書いてくれますから。注意点としては、proxyパターンを使っているときに、例えば ABCToken をデプロイしたので NEXT_PUBLIC_CONTRACT_ABCTOKEN 環境変数を作成したいのに、 NEXT_PUBLIC_ERC1967PROXY なんてproxyコントラクト名の環境変数を作成されても困ります。なので使ってるパターンやproxyの種類に応じてスクリプトを調整しましょう。大丈夫です、まあAIに言えば略 さて、環境変数一覧ができあがりましたから、それを実際にVercelに反映しましょう。反映するのは 作られたテストネットのRPC URL 作られたテストネットのChain ID デプロイされたコントラクトのアドレス(複数) です。ちょっと長いのでまあ参考程度に。一点気をつけるところとしては、 FORCE_BUILD_KEY というのをいじっていますが、これは上の vercel.json でデプロイする条件を厳しくしているので、その条件をこの後通過するよう(e.g. ./scripts/vercel-ignore.sh が 1 を返すように) 1 true などの値に設定しています。 jobs: qa: steps: ... - name: Upsert Vercel env (RPC/ChainId/Contracts + force-build=1) if: inputs.run_vercel && inputs.vercel_update_env env: VERCEL_TOKEN: ${{ secrets.vercel_token }} VERCEL_PROJECT: ${{ secrets.vercel_project }} VERCEL_ENV_TARGET: ${{ inputs.vercel_env_target }} FORCE_BUILD_KEY: ${{ inputs.force_build_key }} NEXT_PUBLIC_QA_RPC_URL_KEY: ${{ inputs.next_public_rpc_key }} NEXT_PUBLIC_QA_CHAIN_ID_KEY: ${{ inputs.next_public_chain_key }} RPC_URL: ${{ env.RPC_URL }} CHAIN_ID: ${{ env.CHAIN_ID }} run: | set -euo pipefail if [ -z "${VERCEL_TOKEN:-}" ] || [ -z "${VERCEL_PROJECT:-}" ]; then echo "ERROR: missing Vercel secrets (vercel_token/vercel_project)." >&2 exit 1 fi if ! command -v jq >/dev/null 2>&1; then sudo apt-get update -y sudo apt-get install -y jq fi # Read contract envs from the persisted file (preferred) or from current environment (fallback). CONTRACT_KVS="$(cat /tmp/contracts.env 2>/dev/null | grep '^CONTRACT_' || true)" if [ -z "$CONTRACT_KVS" ]; then CONTRACT_KVS="$(env | grep '^CONTRACT_' || true)" fi payload=$(jq -n \ --arg target "${VERCEL_ENV_TARGET}" \ --arg forceKey "${FORCE_BUILD_KEY}" \ --arg rpcKey "${NEXT_PUBLIC_QA_RPC_URL_KEY}" \ --arg chainKey "${NEXT_PUBLIC_QA_CHAIN_ID_KEY}" \ --arg rpcVal "${RPC_URL}" \ --arg chainVal "${CHAIN_ID}" \ '[ {key:$forceKey, value:"1", type:"encrypted", target:[$target]}, {key:$rpcKey, value:$rpcVal, type:"encrypted", target:[$target]}, {key:$chainKey, value:$chainVal, type:"encrypted", target:[$target]} ]' ) while IFS= read -r line; do [ -z "$line" ] && continue key="${line%%=*}" val="${line#*=}" payload=$(jq \ --arg target "${VERCEL_ENV_TARGET}" \ --arg k "NEXT_PUBLIC_${key}" \ --arg v "${val}" \ '. + [{key:$k, value:$v, type:"encrypted", target:[$target]}]' \ <<<"$payload" ) done <<<"$CONTRACT_KVS" echo "$payload" > /tmp/vercel-env-payload.json curl -sS -X POST "https://api.vercel.com/v10/projects/${VERCEL_PROJECT}/env?upsert=true" \ -H "Authorization: Bearer ${VERCEL_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary @/tmp/vercel-env-payload.json Vercelのデプロイが走る Vercelにはgit pushやVercel CLI/UIでの手動デプロイの他に、Deploy Hookを呼ぶことでデプロイするという方法が用意されているので、それを使います。 Settings → Gitにあります。 - name: Trigger Vercel Deploy Hook if: inputs.run_vercel env: HOOK: ${{ secrets.vercel_deploy_hook_url }} run: | set -euo pipefail if [ -z "${HOOK:-}" ]; then echo "ERROR: missing vercel_deploy_hook_url secret." >&2 exit 1 fi curl -sS -X POST "$HOOK" これでデプロイされたNext.jsアプリは新しく作られた専用テストネットを向いていて、新規追加や変更されたコントラクトにアクセスできています。一度仕組み化してしまえば、Git pushだけで全てが起こります。 一応、フロントエンド上の設定例は以下です。 import type { Chain } from 'viem' ... const rpcUrl = process.env.NEXT_PUBLIC_QA_RPC_URL const chainIdRaw = process.env.NEXT_PUBLIC_QA_CHAIN_ID if (!rpcUrl || !chainIdRaw) return null const id = Number(chainIdRaw) if (!Number.isFinite(id) || id <= 0) return null const chain: Chain = { id, name: 'Tenderly QA', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: [rpcUrl] }, public: { http: [rpcUrl] }, }, testnet: true, } The Graph連携 詳細は割愛しますが、その新規テストネット上で動くsubgraphのデプロイも自動化で来ます。ここに公式ドキュメントがあるのでどうぞ! 以上、Ethereum開発が楽になった話でした。 余談ですが、この仕組みの構築にかかった時間は数時間でした。数時間でこれが達成できる理由は言わずもがなですが、 Tenderly, Vercel, Foundryなど素晴らしいツールが揃っていること AIがコードをすぐに書いてくれること 自分がAIに適切な指示ができること です。適切な指示にはやはり経験が物を言います。できるかどうかも見当がつかないことを指示したり質問するのは人間には難しいためです。こういう記事を読んで、新しくEthereum開発の世界に入ってくる人の知識・経験の足しになり、サービスを開発していく助けになれば嬉しいです。 最後に、TanéではEthereumエコシステム上でサービスを作る仲間を募集しています。興味のある方はXのDMあたりで連絡ください。よろしくお願いします! ## Publication Information - [0xtomo](https://paragraph.com/@0xtomo/): Publication homepage - [All Posts](https://paragraph.com/@0xtomo/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@0xtomo): Subscribe to updates - [Twitter](https://twitter.com/HAIL): Follow on Twitter