<100 subscribers
Share Dialog
Share Dialog


こんにちは。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
果たしたい目的は以下を考えます:
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環境のできあがり!
これはなんていうか別に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の設定ファイルコントラクトの変更やそれが動くテストネットの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だけでデプロイが動いてほしいか、ほしくないかを記述しました。例えば、本番環境やステージングは動いてほしいとか、この環境変数が立っているときはしてほしいとか、開発のステージや要件次第。
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 \
-vvvforge 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" || truescripts/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.jsonVercelには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,
}詳細は割愛しますが、その新規テストネット上で動くsubgraphのデプロイも自動化で来ます。ここに公式ドキュメントがあるのでどうぞ!
以上、Ethereum開発が楽になった話でした。
余談ですが、この仕組みの構築にかかった時間は数時間でした。数時間でこれが達成できる理由は言わずもがなですが、
Tenderly, Vercel, Foundryなど素晴らしいツールが揃っていること
AIがコードをすぐに書いてくれること
自分がAIに適切な指示ができること
です。適切な指示にはやはり経験が物を言います。できるかどうかも見当がつかないことを指示したり質問するのは人間には難しいためです。こういう記事を読んで、新しくEthereum開発の世界に入ってくる人の知識・経験の足しになり、サービスを開発していく助けになれば嬉しいです。
最後に、TanéではEthereumエコシステム上でサービスを作る仲間を募集しています。興味のある方はXのDMあたりで連絡ください。よろしくお願いします!
こんにちは。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
果たしたい目的は以下を考えます:
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環境のできあがり!
これはなんていうか別に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の設定ファイルコントラクトの変更やそれが動くテストネットの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だけでデプロイが動いてほしいか、ほしくないかを記述しました。例えば、本番環境やステージングは動いてほしいとか、この環境変数が立っているときはしてほしいとか、開発のステージや要件次第。
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 \
-vvvforge 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" || truescripts/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.jsonVercelには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,
}詳細は割愛しますが、その新規テストネット上で動くsubgraphのデプロイも自動化で来ます。ここに公式ドキュメントがあるのでどうぞ!
以上、Ethereum開発が楽になった話でした。
余談ですが、この仕組みの構築にかかった時間は数時間でした。数時間でこれが達成できる理由は言わずもがなですが、
Tenderly, Vercel, Foundryなど素晴らしいツールが揃っていること
AIがコードをすぐに書いてくれること
自分がAIに適切な指示ができること
です。適切な指示にはやはり経験が物を言います。できるかどうかも見当がつかないことを指示したり質問するのは人間には難しいためです。こういう記事を読んで、新しくEthereum開発の世界に入ってくる人の知識・経験の足しになり、サービスを開発していく助けになれば嬉しいです。
最後に、TanéではEthereumエコシステム上でサービスを作る仲間を募集しています。興味のある方はXのDMあたりで連絡ください。よろしくお願いします!
No comments yet