compose-vrf について
Compose VRFは、EVMコントラクトの乱数要求を受け、drandビーコンの検証可能な乱数で処理するサンプルアプリを構築するスキルです。Base Sepoliaの共有デモコントラクトまたは独自コントラクトに対応し、Composeでのデプロイ手順も示します。
主な機能
- 01オンチェーンの乱数要求イベントを監視
- 02drandから検証可能な乱数を取得
- 03EVMコントラクトへ乱数結果を送信
- 04共有またはユーザー独自のコントラクトに対応
- 05アプリのソースとデプロイ手順を提供
活用シーン
- 01Base Sepoliaで検証可能な乱数デモを構築
- 02互換性のあるEVMコントラクトをdrand乱数に接続
- 03乱数要求向けComposeアプリを設定
- 04サンプルをユーザー独自のコントラクト向けに調整
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ goldsky-io/ goldsky-agent --skill compose-vrfインストール後、Claude Code で/compose-vrfを入力して読み込まれたか確認します。
詳細
ビルド:Compose VRF
ユーザー自身の Goldsky アカウントで VRF の例を起動します。アプリは EVM コントラクト上の RandomnessRequested(uint256,address) イベントをリッスンし、drand ビーコンから検証可能なランダム性を取得して(BLS12-381 のしきい値署名で、誰でも検証できます)、fulfillRandomness(requestId, randomness, round, signature) を使ってオンチェーンに書き戻します。信頼性は呼び出し元ではなく、保存された drand の round と signature に基づくため、共有のサンプルコントラクトは公開状態のままでも安全です。
このテンプレートでは、VRF アプリ固有の内容、つまりその仕組みと完全なソースコード(以下)のみを提供します。推奨する方法では、Base Sepolia 上の共有され、完全にアクセス制限のない RandomnessConsumer(0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6d)を使用するため、ユーザーは何もデプロイする必要がなく、Compose スマートウォレットが自動作成され、ガス代もスポンサーされます。
ステップ 0a — 最初に基本スキルを読み込む
何をするよりも先に――回答する、質問する、ファイルを作成する、deployComposeApp を呼び出すよりも先に、このテンプレートが依存する 2 つの基本スキルを読み込んでください。
Skill(compose)— 常時適用される Compose ガイドです。基本ルール(ユーザーに代わってアプリについて決めつけないこと、不明な点は質問すること)と、ビルドに関する一般的なガイダンスが含まれています。Skill(compose-reference)— マニフェスト、フィールド、API のリファレンスです。compose.yamlまたはタスクファイルを作成する前に参照してください。
このテンプレートでは、意図的にこれらのルールとリファレンスを省略しています。正しくビルドするために必須ですが、ここでは繰り返しません。両方を読み込むまで先に進まないでください。
モードの検出
利用可能なツールからモードを選択します。
-
deployComposeAppツールが利用可能(Goldsky Web アプリのチャットボット)— アプリ内での推奨フローです。goldskyのターミナルコマンドやcliCommandカードは出力しないでください。また、Step 0b/degit/forge/goldsky compose deployは使用しないでください。代わりに、2~3 文で簡潔に説明してから、askUserで以下の質問をします(推奨オプションにはrecommendedIndexを指定します)。- アプリ名 — 何よりも先に質問します:"アプリの名前は何にしますか?(
vrf-appをおすすめします)"。名前は後から変更しにくく、名前付きウォレットのスコープとなり、すべてのdeployContractの CREATE2 ソルトに関係するため、ウォレットやコントラクトの手順に進む前に決定する必要があります。ユーザーが迷っている場合はデフォルトのvrf-appを受け入れ、選択した名前を、生成するcompose.yamlのトップレベルのname:に設定します。 - コントラクト — 必ず明示的に質問し、推測しないでください:"独自の
RandomnessConsumerスタイルのコントラクトをお持ちですか?それとも、すぐに使えるよう Base Sepolia 上の共有デモコントラクトを使用しますか?" 選択肢は、**「Base Sepolia 上の共有デモコントラクトを使用する(おすすめ — デプロイ不要)」と「独自のコントラクトを使用します」**です。共有コントラクトを使う場合、CONTRACT_ADDRESSはbaseSepolia上の ハードコードされた アドレス0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6dです。文字単位でそのままコピーし、変更したり記憶を頼りに入力し直したりしないでください。これはデモ専用であり、本番環境向けではないことを説明します。独自コントラクトを使う場合は、コントラクトアドレスとチェーンを貼り付けるようユーザーに依頼します(コントラクトはRandomnessRequested(uint256,address)を発行し、fulfillRandomnessを受け付ける必要があります)。入力された内容をそのまま使用します。
Compose スマートウォレットは実行時に自動作成され、Base Sepolia 上でガス代がスポンサーされます。ユーザーにウォレットの作成や資金の入金を指示しないでください。共有コントラクトのパスでは(コントラクトはパーミッションレスのため)設定するフルフィラーはありません。そのため、ウォレットはそのまま機能します。質問後、以下の「The app (full source)」をもとに、ファイルをメモリ内で生成して
deployComposeAppに渡します(degitは使用しないでください):compose.yaml、src/contracts/RandomnessConsumer.json(コード生成に必要な ABI をそのまま使用)、src/lib/drand.ts、および 3 つのタスクファイル。トップレベルのname:は選択したアプリ名(デフォルトはvrf-app)に設定します。両方のタスクファイルのCONTRACT_ADDRESSとcompose.yamlのcontract:フィールドには、共有アドレス(または独自コントラクトのパスではユーザーが指定したアドレス)を設定します。また、evm.chains.*の参照とトリガーのnetwork:は、選択したチェーンに設定します。ファイルを出力する前に、マニフェストの構造とサンドボックスのインポート規則について/compose-referenceに従います(/composeのゴールデンルールに従い、マニフェストを記憶だけで作成しないでください)。その後、同じターン内でdeployComposeAppを呼び出します。先にユーザーの確認を求めたり、goldskyコマンドを出力したりしないでください。このモードでは、以下の Step 0~8 は無視してください(CLI/ローカルでの手順です)。 - アプリ名 — 何よりも先に質問します:"アプリの名前は何にしますか?(
-
Bashが利用可能(ローカル CLI/コーディングエージェント): 以下の手順を直接実行し、出力を解析して後続のコマンドに使用します。 -
どちらも利用できない(参照情報のみの Q&A): アプリの機能を説明します。手順に沿ったサポートが必要な場合は、ローカルで Bash を使って実行できるよう、
npx skills add goldsky-io/goldsky-agentを案内します。
必須事項
- Base Sepolia 上の共有コントラクト
0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6dは完全にパーミッションレスで、誰でもfulfillRandomnessを呼び出せます。 ここでは安全です。オンチェーンに保存された drand のroundと BLSsignatureによってのみ乱数を信頼しており、誰でも drand quicknet の公開鍵を使ってオフチェーンで検証できるためです。これはセットアップとデモのためだけに存在します。本番環境では使用してはならないこと、Base Sepolia にのみ存在することを、ユーザーに文章で伝えてください。 - デプロイ済みの共有コントラクト
0x6273AB…は、動作するfulfiller()/setFulfillerを公開していません。 上記の参照ソースには含まれていますが、デプロイ済みバイトコードが異なるため、これらの呼び出しはオンチェーンでリバートします。アプリには影響しません(重要なのはfulfillRandomnessであり、これはパーミッションレスです)。そのため、fulfiller()/setFulfillerを呼び出して共有コントラクトを「検証」してはならず、これらの呼び出しがリバートしても障害として扱わないでください。 - アプリの接続先に
0x53...または0xE05Ceb3E269029E3bab46E35515e8987060D1027を指定してはなりません。 その古いデモコントラクトはパーミッションが設定されており(fulfillerは固定アドレスで、ユーザーの Compose ウォレットではありません)、既製のfulfillRandomnessはすべてOnlyFulfillerでリバートします。デプロイ不要の共有コントラクトは0x6273AB...のみで、ほかにはありません。 - コントラクトアドレスを共有する箇所は3つあります。
compose.yamlのcontract:フィールドと、src/tasks/fulfill-randomness.tsおよびsrc/tasks/request-randomness.tsのCONTRACT_ADDRESSです。ユーザーが変更する場合は、3か所すべてを変更してください。 - 独自デプロイの手順のみ: 参照コントラクトでは drand による fulfillment はパーミッションレスですが、ユーザー独自のコントラクトで fulfillment が制限されている場合は、権限を持つ fulfiller を Compose ウォレットに設定する必要があります。そうしないと、
fulfillRandomnessはすべてリバートします。共有コントラクトにはそのような制限はありません。 - 正確なコマンドを先に提示し、明示的な確認を得ることなく、
goldsky compose deployContract、goldsky compose deploy、git push、gh repo createを実行してはなりません。 - リテラルの秘密鍵や API キーをコマンドライン、チャットに表示するファイル、またはトランスクリプトの出力に含めてはなりません。 キーは chmod-600 のファイル(
.env、.eoa.env)に保存し、単一の Bash 呼び出し内でsourceと"$VAR"によるシェル展開のみを使って参照してください。このスキルで$SOME_KEYと記載されている場合は、シェル内で実行するシェル展開を指しており、値を取得して置き換えるものではありません。
アプリ(ソース全体)
これは完全な VRF アプリです。アプリ内フローでは、インタビュー内容に応じてアドレス/チェーンを設定し、以下のファイルをメモリ上にそのままひな形として作成してください。CLI フローでは、Step 0b で同じファイルを goldsky-io/documentation-examples から取得します。ただし、ひな形に組み込まれた既定値は古く、デプロイ前に必ず差し替える必要があります(Step 4 は任意ではなく必須です)。具体的には、ひな形の compose.yaml と両方のタスクファイルには name: "compose-vrf"(古い値です。Step 1 で選択したアプリ名で上書きしてください。既定値は vrf-app)と CONTRACT_ADDRESS = 0xE05Ceb3E269029E3bab46E35515e8987060D1027(このスキルで使用を禁じている、パーミッションが設定されたコントラクト)が含まれています。ひな形の値を信用せず、Step 4 ですべて差し替えてください。推奨手順では、ソース全体に記載されているデプロイ不要の共有コントラクト 0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6d を、Step 4 で設定します。
compose.yaml
name: "vrf-app"
api_version: "stable"
tasks:
# Utility task to get the Compose wallet address (only needed if deploying your own contract)
- path: "./src/tasks/generate-wallet.ts"
name: "generate_wallet"
triggers:
- type: "http"
authentication: "auth_token"
# HTTP endpoint to request randomness (no MetaMask needed)
- path: "./src/tasks/request-randomness.ts"
name: "request_randomness"
triggers:
- type: "http"
authentication: "auth_token"
# Main fulfillment task — triggered by the on-chain RandomnessRequested event
- path: "./src/tasks/fulfill-randomness.ts"
name: "fulfill_randomness"
triggers:
- type: "onchain_event"
network: "base_sepolia"
# Shared, fully-unpermissioned RandomnessConsumer (anyone can fulfill) — nothing to deploy.
# Keep in sync with CONTRACT_ADDRESS in the two task files.
contract: "0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6d"
events:
- "RandomnessRequested(uint256,address)"
retry_config: # matches the CLI default; not an override
max_attempts: 3
initial_interval_ms: 1000
backoff_factor: 2
src/tasks/fulfill-randomness.ts
import { TaskContext, OnchainEvent } from "compose";
import {
fetchLatestRandomness,
toBytes32,
toBytes,
DRAND_CHAIN_INFO,
} from "../lib/drand.ts";
// Shared, fully-unpermissioned RandomnessConsumer on Base Sepolia — anyone can
// fulfill, so there's nothing to deploy and no wallet to whitelist. Swap for
// your own contract to go to production. Keep in sync with the CONTRACT_ADDRESS
// in request-randomness.ts and the `contract:` field in compose.yaml.
const CONTRACT_ADDRESS = "0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6d";
/**
* Fulfill randomness requests using drand.
*
* Triggered by the on-chain RandomnessRequested event (configured in compose.yaml).
* Fetches verifiable randomness from drand and writes it back to the target contract.
*/
export async function main(context: TaskContext, event?: OnchainEvent) {
const { fetch, evm } = context;
// Parse request ID from event topics
const requestId = event?.topics[1] ? BigInt(event.topics[1]) : 0n;
// Fetch randomness from drand
const drandResponse = await fetchLatestRandomness(fetch);
console.log(`fetched drand round ${drandResponse.round}`);
// Get wallet and instantiate typed contract (generated from src/contracts/RandomnessConsumer.json)
const wallet = await evm.wallet({
name: "randomness-fulfiller",
});
const contract = new evm.contracts.RandomnessConsumer(
CONTRACT_ADDRESS,
evm.chains.baseSepolia,
wallet
);
// Prepare the fulfillment arguments
const randomnessBytes32 = toBytes32(drandResponse.randomness);
const signatureBytes = toBytes(drandResponse.signature);
// Fulfill the randomness request on-chain
const { hash } = await contract.fulfillRandomness(
requestId.toString(),
randomnessBytes32,
drandResponse.round,
signatureBytes
);
console.log(`fulfilled request ${requestId} in tx ${hash}`);
return {
success: true,
requestId: requestId.toString(),
transactionHash: hash,
drand: {
round: String(drandResponse.round),
randomness: randomnessBytes32,
chainHash: DRAND_CHAIN_INFO.hash,
},
};
}
src/tasks/request-randomness.ts
import { TaskContext } from "compose";
// Shared, fully-unpermissioned RandomnessConsumer on Base Sepolia — anyone can
// request. Keep in sync with the CONTRACT_ADDRESS in fulfill-randomness.ts and
// the `contract:` field in compose.yaml.
const CONTRACT_ADDRESS = "0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6d";
export async function main(context: TaskContext): Promise<{
requestId: string;
txHash: string;
}> {
const { evm } = context;
const wallet = await evm.wallet({
name: "randomness-requester",
});
// Instantiate typed contract (generated from src/contracts/RandomnessConsumer.json)
const contract = new evm.contracts.RandomnessConsumer(
CONTRACT_ADDRESS,
evm.chains.baseSepolia,
wallet
);
// Send the request transaction
const { hash } = await contract.requestRandomness();
// Read nextRequestId after tx — subtract 1 to get our requestId
const nextId = await contract.nextRequestId();
const requestId = String(BigInt(nextId) - 1n);
return {
requestId,
txHash: hash,
};
}
src/tasks/generate-wallet.ts
import { TaskContext } from "compose";
/**
* Generate the Compose wallet and output its address.
*
* Only needed on the deploy-your-own path: the fulfiller address comes from
* Step 3 Branch B's `goldsky compose wallet create randomness-fulfiller`.
* `callTask` targets the DEPLOYED app by default:
* goldsky compose callTask generate_wallet '{}' # deployed app
* goldsky compose callTask generate_wallet '{}' --env local # local `compose start` server
*
* (The upstream goldsky-io/documentation-examples repo ships the same stale
* "run this before deploying" comment — do NOT fix the upstream repo here.)
*/
export async function main(context: TaskContext) {
const { evm } = context;
const wallet = await evm.wallet({ name: "randomness-fulfiller" });
return {
address: wallet.address,
name: wallet.name,
message: "Use this address as the fulfiller when deploying your contract",
};
}
src/lib/drand.ts
/**
* drand API utilities for fetching verifiable randomness.
*
* drand produces BLS12-381 threshold signatures that anyone can verify.
* The randomness is sha256(signature), making it deterministic and verifiable.
*/
// ============ Types ============
export type DrandResponse = {
round: number;
randomness: string; // hex - sha256(signature)
signature: string; // hex - BLS12-381 signature (96 bytes)
previous_signature: string;
};
export type DrandChainInfo = {
hash: string;
publicKey: string;
genesisTime: number;
period: number;
};
// ============ Constants ============
/**
* drand quicknet chain info (3 second rounds).
* Use these values to verify randomness off-chain.
*
* Note: Quicknet uses "unchained" randomness (no previous_signature linking)
* and BLS signatures on G1 curve instead of G2.
*/
export const DRAND_CHAIN_INFO: DrandChainInfo = {
hash: "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
publicKey:
"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a",
genesisTime: 1692803367,
period: 3, // seconds between rounds
};
export const DRAND_API_URL =
"https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971";
// ============ Functions ============
/**
* Fetch the latest randomness from drand.
*/
export async function fetchLatestRandomness(
fetchFn: <T>(url: string) => Promise<T | undefined>
): Promise<DrandResponse> {
const response = await fetchFn<DrandResponse>(
`${DRAND_API_URL}/public/latest`
);
if (!response) {
throw new Error("Failed to fetch randomness from drand");
}
return response;
}
/**
* Fetch randomness for a specific round.
*/
export async function fetchRandomnessForRound(
fetchFn: <T>(url: string) => Promise<T | undefined>,
round: number
): Promise<DrandResponse> {
const response = await fetchFn<DrandResponse>(
`${DRAND_API_URL}/public/${round}`
);
if (!response) {
throw new Error(`Failed to fetch randomness for round ${round}`);
}
return response;
}
/**
* Convert hex string to bytes32 format (with 0x prefix, 64 chars).
*/
export function toBytes32(hex: string): `0x${string}` {
// Remove 0x prefix if present
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
// Pad to 64 characters (32 bytes)
const padded = clean.padStart(64, "0");
return `0x${padded}` as `0x${string}`;
}
/**
* Convert hex string to bytes (with 0x prefix).
*/
export function toBytes(hex: string): `0x${string}` {
const clean = hex.startsWith("0x") ? hex : `0x${hex}`;
return clean as `0x${string}`;
}
src/contracts/RandomnessConsumer.json(ABI — コード生成に必須)
これをそのまま書き込んでください。コントラクトのコード生成で読み込まれ、evm.contracts.RandomnessConsumer が構築されます。ABI を作り出してはなりません。
[
{ "type": "constructor", "inputs": [{ "name": "_fulfiller", "type": "address", "internalType": "address" }], "stateMutability": "nonpayable" },
{ "type": "function", "name": "fulfillRandomness", "inputs": [{ "name": "requestId", "type": "uint256", "internalType": "uint256" }, { "name": "randomness", "type": "bytes32", "internalType": "bytes32" }, { "name": "round", "type": "uint64", "internalType": "uint64" }, { "name": "signature", "type": "bytes", "internalType": "bytes" }], "outputs": [], "stateMutability": "nonpayable" },
{ "type": "function", "name": "fulfiller", "inputs": [], "outputs": [{ "name": "", "type": "address", "internalType": "address" }], "stateMutability": "view" },
{ "type": "function", "name": "getRandomness", "inputs": [{ "name": "requestId", "type": "uint256", "internalType": "uint256" }], "outputs": [{ "name": "randomness", "type": "bytes32", "internalType": "bytes32" }, { "name": "round", "type": "uint64", "internalType": "uint64" }, { "name": "signature", "type": "bytes", "internalType": "bytes" }], "stateMutability": "view" },
{ "type": "function", "name": "isFulfilled", "inputs": [{ "name": "requestId", "type": "uint256", "internalType": "uint256" }], "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], "stateMutability": "view" },
{ "type": "function", "name": "nextRequestId", "inputs": [], "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], "stateMutability": "view" },
{ "type": "function", "name": "requestRandomness", "inputs": [], "outputs": [{ "name": "requestId", "type": "uint256", "internalType": "uint256" }], "stateMutability": "nonpayable" },
{ "type": "function", "name": "requests", "inputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], "outputs": [{ "name": "requester", "type": "address", "internalType": "address" }, { "name": "fulfilled", "type": "bool", "internalType": "bool" }, { "name": "randomness", "type": "bytes32", "internalType": "bytes32" }, { "name": "round", "type": "uint64", "internalType": "uint64" }, { "name": "signature", "type": "bytes", "internalType": "bytes" }], "stateMutability": "view" },
{ "type": "function", "name": "setFulfiller", "inputs": [{ "name": "_fulfiller", "type": "address", "internalType": "address" }], "outputs": [], "stateMutability": "nonpayable" },
{ "type": "event", "name": "RandomnessFulfilled", "inputs": [{ "name": "requestId", "type": "uint256", "indexed": true, "internalType": "uint256" }, { "name": "randomness", "type": "bytes32", "indexed": false, "internalType": "bytes32" }, { "name": "round", "type": "uint64", "indexed": false, "internalType": "uint64" }, { "name": "signature", "type": "bytes", "indexed": false, "internalType": "bytes" }], "anonymous": false },
{ "type": "event", "name": "RandomnessRequested", "inputs": [{ "name": "requestId", "type": "uint256", "indexed": true, "internalType": "uint256" }, { "name": "requester", "type": "address", "indexed": true, "internalType": "address" }], "anonymous": false },
{ "type": "error", "name": "AlreadyFulfilled", "inputs": [] },
{ "type": "error", "name": "OnlyFulfiller", "inputs": [] },
{ "type": "error", "name": "RequestNotFound", "inputs": [] }
]
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "dom"],
"module": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"compose": [".compose/types.d.ts"]
}
},
"include": ["src/**/*"]
}
contracts/RandomnessConsumer.sol(独自デプロイする場合のみ)
参照用コントラクトです。設計上、誰でもフルフィルメントを実行できます。詳細は fulfillRandomness の NatSpec を参照してください。fulfiller フィールドはデプロイ時の情報ラベルであり、アクセス制限ではありません。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title RandomnessConsumer
* @notice Example contract demonstrating the drand randomness request/fulfill pattern.
* @dev Users can replace this with their own contract — just emit an event and implement fulfillment.
*
* Verification: the randomness is verifiable using drand's BLS12-381 signatures.
* Chain info for verification (drand quicknet):
* - Chain Hash: 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971
* - Public Key: 83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a
*/
contract RandomnessConsumer {
struct RandomnessRequest {
address requester;
bool fulfilled;
bytes32 randomness;
uint64 round;
bytes signature;
}
/// @notice The Compose wallet recorded at deploy time (informational label only).
address public fulfiller;
/// @notice Counter for generating request IDs.
uint256 public nextRequestId;
/// @notice Mapping of request ID to request data.
mapping(uint256 => RandomnessRequest) public requests;
/// @notice Emitted when randomness is requested — Compose listens for this.
event RandomnessRequested(uint256 indexed requestId, address indexed requester);
/// @notice Emitted when randomness is fulfilled with full proof data.
event RandomnessFulfilled(uint256 indexed requestId, bytes32 randomness, uint64 round, bytes signature);
error OnlyFulfiller();
error RequestNotFound();
error AlreadyFulfilled();
/// @param _fulfiller The Compose wallet address recorded as the deploy-time fulfiller label.
constructor(address _fulfiller) {
fulfiller = _fulfiller;
}
/**
* @notice Request randomness — emits the event that Compose listens to.
* @return requestId The ID of this request.
*/
function requestRandomness() external returns (uint256 requestId) {
requestId = nextRequestId++;
requests[requestId] = RandomnessRequest({
requester: msg.sender,
fulfilled: false,
randomness: bytes32(0),
round: 0,
signature: ""
});
emit RandomnessRequested(requestId, msg.sender);
}
/**
* @notice Fulfill a randomness request with drand proof data.
* @dev Permissionless: any caller may fulfill so the shared example contract is
* reusable by anyone without being whitelisted. Trust does not come from the
* caller's identity, it comes from the stored drand `round` + `signature`,
* which anyone can verify off-chain against the drand quicknet BLS public key.
*/
function fulfillRandomness(
uint256 requestId,
bytes32 randomness,
uint64 round,
bytes calldata signature
) external {
RandomnessRequest storage request = requests[requestId];
if (request.requester == address(0)) revert RequestNotFound();
if (request.fulfilled) revert AlreadyFulfilled();
request.fulfilled = true;
request.randomness = randomness;
request.round = round;
request.signature = signature;
emit RandomnessFulfilled(requestId, randomness, round, signature);
}
function getRandomness(uint256 requestId)
external
view
returns (bytes32 randomness, uint64 round, bytes memory signature)
{
RandomnessRequest storage request = requests[requestId];
return (request.randomness, request.round, request.signature);
}
function isFulfilled(uint256 requestId) external view returns (bool) {
return requests[requestId].fulfilled;
}
/// @notice Update the fulfiller label (for key rotation); does not gate fulfillment.
function setFulfiller(address _fulfiller) external {
if (msg.sender != fulfiller) revert OnlyFulfiller();
fulfiller = _fulfiller;
}
}
以下のステップ 0~8 は、Bash/ローカル CLI の手順です。
deployComposeAppツールを利用できる場合(Web アプリのチャットボット)は、これらの手順に従わず、上記の「モードの判定」にあるデプロイツールのフローを使用してください。
ステップ 0b — サンプルをひな形から作成する
VRF サンプルのみを新しいディレクトリに取得します(Git の履歴は含みません)。
npx -y degit goldsky-io/documentation-examples/compose/VRF#6abe62878cb92f2569538ba9572b049ed5949a01 compose-vrf
cd compose-vrf
npx -y degit を利用できない場合は、代わりにスパースクローンを実行します。
git clone --filter=blob:none --sparse https://github.com/goldsky-io/documentation-examples.git
cd documentation-examples && git checkout 6abe62878cb92f2569538ba9572b049ed5949a01 && git sparse-checkout set compose/VRF && cd compose/VRF
ユーザーがすでにサンプルをクローンしている場合は、このステップを省略して、そのディレクトリに cd します。いずれの場合も、ユーザーが独自にデプロイする場合(ステップ 3、分岐 B)を除き、両方のタスクファイルの CONTRACT_ADDRESS と compose.yaml の contract: フィールドに、デプロイ不要の共通アドレス 0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6d を設定してください。
事前確認
goldsky compose --version の出力が compose CLI is not installed の場合は、goldsky compose install を実行してから再確認してください(stable チャンネルでは何度実行しても安全です)。goldsky CLI、認証、deno の確認は、標準の Compose 事前確認です。/compose と /auth-setup を参照してください。認証については、シェルで一度 GOLDSKY_API_TOKEN=<project API key> をエクスポートする方法(推奨:キーが argv やシェル履歴に残りません)、コマンドごとに -t "$GOLDSKY_PROJECT_KEY" を渡す方法、または有効な goldsky login セッションを使う方法があります。優先順位は --token > GOLDSKY_API_TOKEN > ~/.goldsky/auth_token です。バージョン確認(最初に実施してください): goldsky compose --version を実行します。goldsky compose 0.8.1(またはそれ以降)が表示されます。バージョンが 0.8.1 より古い場合、または goldsky compose deployContract/goldsky compose writeContract が不明なコマンドとして報告される場合、このスキルを実行するには CLI が古すぎます。goldsky compose update(特定のバージョンを指定する場合は goldsky compose update 0.8.1)の実行を提案し、完了後に --version を再確認してください。0.8.1 以降であることを確認するまで、先に進まないでください。
VRF 固有の Foundry に関する注意:推奨パスでは Foundry(forge/cast)は不要です。deployContract はクラウドの Alchemy バンドラーを経由し、Ethereum、Sepolia、Polygon、Polygon Amoy、Arbitrum、Arbitrum Sepolia、Optimism、Optimism Sepolia、Base、Base Sepolia(チェーン ID 1、11155111、137、80002、42161、421614、10、11155420、8453、84532)に対応します。この一覧にないチェーンでは No Alchemy bundler URL for chain <id> というエラーが発生するため、その場合は forge create による代替手段を使用してください。複数プロバイダーへの対応状況は FOU-991 で追跡されています。分岐 B の goldsky compose deployContract はコンパイラーを内蔵しており、ステップ 8 のスモークテストではガス代をスポンサーする goldsky compose writeContract を使用します。Foundry が必要なのは、forge による代替手段、またはステップ 8 の cast ベースのスモークテスト代替手段を使用する場合です。そのため、推奨パスでは Foundry は任意としてください。
ステップ 1 — 設定のヒアリング
質問は一度に1つずつ行い、それぞれの回答を次の質問に反映します。読みやすいラベルを使い、機械用の値には自分で変換します。
- アプリ名 — ウォレットやコントラクトに関する手順より先に、必ず最初に質問します。"アプリの名前は何にしますか?(
vrf-appをおすすめします)"。名前は後から変更しにくく、名前付きウォレットのスコープとなり、すべてのdeployContractで CREATE2 ソルトに使用されます。迷っている場合は既定値のvrf-appを使い、compose.yamlのトップレベルのname:に設定します(degit のスキャフォールドには古いname: "compose-vrf"が含まれているため、上書きします)。 - 「どのチェーンを使いますか?」 — **Base Sepolia(推奨)**を選びます。すぐに使える、完全に無許可で利用できる共有コントラクトがあるためです(デプロイ不要)。ほかのチェーンでは独自のコントラクトをデプロイする必要があります。TS ではキャメルケース形式(
baseSepolia)、compose.yamlではスネークケース形式(base_sepolia)を使います。 - 「RandomnessConsumer コントラクトはどうしますか?」(チェーンを決めた直後に質問します)— 次の2つの選択肢を提示します。
- Base Sepolia の共有コントラクトを再利用する(推奨) — デプロイ不要です。
0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6dを設定します(このアドレスは選択肢のラベルではなく、説明文で示します)。デモ/導入時のみを対象としており、本番環境向けではありません。 - 独自のコントラクトをデプロイする — ステップ 3 の分岐 B を参照してください。(Base Sepolia 以外のチェーンでは必須です。)
- Base Sepolia の共有コントラクトを再利用する(推奨) — デプロイ不要です。
ステップ 2 — ウォレット
- 共有コントラクトを使う場合(推奨): 作業は不要です。Compose スマートウォレットは実行時に自動作成され、Base Sepolia ではガス代がすべてスポンサーされます。また、共有コントラクトは無許可で利用できるため、承認するフルフィラーもありません。ユーザーにウォレットの作成や資金の入金を指示しないでください。
- 独自にデプロイする場合(分岐 B): リファレンスコントラクトの
fulfillerはデプロイ時の説明用ラベルにすぎません(フルフィルメントは無許可で実行可能です)が、コンストラクターにはラベルとして Compose ウォレットのアドレスが必要です。分岐 B のステップ 3 にある順序の(1)として、デプロイ前にウォレットを作成します。アドレスを$COMPOSE_WALLETとして記録してください。名前付きウォレットrandomness-fulfillerは、src/tasks/fulfill-randomness.tsのevm.wallet({ name: "randomness-fulfiller" })に対応します。
ステップ 3 — コントラクト
分岐 A — 共有コントラクトを再利用する(推奨)。 Base Sepolia では $CONTRACT_ADDRESS = 0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6d です。デプロイもフルフィラーの承認も不要です。ステップ 4 に進みます。
分岐 B — 独自のコントラクトをデプロイする。 分岐 B では順序が決まっています。ウォレットの作成 → コントラクトのデプロイ → コード生成 → 設定(ステップ 4)→ アプリのデプロイ(ステップ 7)。以下のステップ(1)~(5)を順に実行します。リファレンスコントラクトは上記の**アプリ(ソース全体)**にあります(contracts/RandomnessConsumer.sol)。deployContract がコンパイル済み ABI を src/contracts/RandomnessConsumer.json に書き込むため、手作業での確認は不要です。
(1)ウォレットを作成し(デプロイ前に実行できます)、そのアドレスを $COMPOSE_WALLET として記録します。これは src/tasks/fulfill-randomness.ts の evm.wallet({ name: "randomness-fulfiller" }) に対応します。
goldsky compose wallet create randomness-fulfiller
⚠
wallet create --env localは使用しないでください。これはローカルの tevm ウォレットであり、クラウドランタイムでは署名に使われません。その結果、アプリが使用不能になりますが、以下の「トラブルシューティング」セクションでは原因を特定できません。既定の(クラウド)ウォレットを使用してください。名前付きウォレットのスコープは、compose.yamlのトップレベルのname:によってアプリ単位で決まります。そのため、この時点でアプリ名(ステップ 1 で決めた名前。例:vrf-app)はすでに確定しています。後から名前を変更すると、コントラクトに組み込まれたものとは異なるウォレットが導出されます。
(2)コントラクトをデプロイします。 deployContract は CLI 内でコンパイルし、ガス代がスポンサーされる Compose ウォレットで署名した CREATE2 プロキシ経由でデプロイするため、Base/Base Sepolia では資金を入金した EOA も RPC URL も必要ありません。コンストラクター引数には、Compose ウォレットを(説明用の)フルフィラーラベルとして記録します。
ADDR=$(goldsky compose deployContract contracts/RandomnessConsumer.sol \
--chain-id <CHAIN_ID> \
--constructor-args $COMPOSE_WALLET \
--wallet randomness-fulfiller \
--json | jq -r .address)
認証: シェルで一度だけ GOLDSKY_API_TOKEN=<project API key> をエクスポートする(推奨:キーが argv とシェル履歴に残りません)。または、コマンドごとに -t "$GOLDSKY_PROJECT_KEY" を渡すか、有効な goldsky login セッションを使用します。優先順位:--token > GOLDSKY_API_TOKEN > ~/.goldsky/auth_token。401 の場合は「Goldsky ダッシュボードの Settings > API Keys から API キーを生成してください」と表示されます。キーがない場合はそこで生成し、ユーザーに -t を指定してデプロイを実行するか提案してください。
チェーン ID とルーティング。deployContract はクラウドの Alchemy バンドラー経由でルーティングされます。対応するのは Ethereum、Sepolia、Polygon、Polygon Amoy、Arbitrum、Arbitrum Sepolia、Optimism、Optimism Sepolia、Base、Base Sepolia(チェーン ID 1、11155111、137、80002、42161、421614、10、11155420、8453、84532)です。この一覧にないチェーンでは、No Alchemy bundler URL for chain <id> というエラーでデプロイに失敗するため、その場合は forge create のフォールバックを使用してください。複数プロバイダーへの対応状況は FOU-991 で追跡されています。
- デプロイ手数料無料 →
deployContractを使用:ethereum→1、sepolia→11155111、polygon→137、polygonAmoy→80002、arbitrum→42161、arbitrumSepolia→421614、optimism→10、optimismSepolia→11155420、base→8453、baseSepolia→84532。
deployContract は ABI を src/contracts/RandomnessConsumer.json に自動保存します。--json を指定すると ASCII アートが抑制されます。失敗時には、コマンドは stderr に {"error":true,"code":"...","message":"..."} を出力して 1 で終了するため、取得結果が空なら stderr を確認してください。CONTRACT_ADDRESS=$ADDR を設定します。
Base 以外のチェーン(forge のフォールバック)。 deployContract はクラウドの Alchemy バンドラー経由でルーティングされます。対応するのは Ethereum、Sepolia、Polygon、Polygon Amoy、Arbitrum、Arbitrum Sepolia、Optimism、Optimism Sepolia、Base、Base Sepolia(チェーン ID 1、11155111、137、80002、42161、421614、10、11155420、8453、84532)です。この一覧にないチェーンでは、No Alchemy bundler URL for chain <id> というエラーでデプロイに失敗するため、その場合は forge create のフォールバックを使用してください。複数プロバイダーへの対応状況は FOU-991 で追跡されています。forge の経路では、フルフィラーのラベルは引き続き $COMPOSE_WALLET なので、タスクのコードに変更はありません。
資金を用意した EOA(手間のかからない生成の提案)。 ユーザーに秘密鍵を尋ねないでください。代わりに、使い捨ての資金を用意した EOA を生成するよう提案してください。モデルが読み取ることのない chmod-600 のファイルに直接生成し、アドレスのみを表示します。
cast wallet new --json > /tmp/k.json
umask 077 && printf 'FUNDED_EOA_KEY=%s\n' "$(jq -r '.[0].private_key' /tmp/k.json)" > .eoa.env
cast wallet address "$(jq -r '.[0].private_key' /tmp/k.json)" # the ONLY thing ever printed
shred -u /tmp/k.json
選択したチェーンに適したフォーセットをユーザーに案内し、デプロイ前に cast balance <address> --rpc-url <RPC> で資金を確認するか提案してください。断られた場合は、代わりに自分で資金を用意したウォレットを使ってデプロイする必要があると、明確に伝えてください。
source .eoa.env # provides FUNDED_EOA_KEY; keep this in the SAME shell invocation as the command
forge create contracts/RandomnessConsumer.sol:RandomnessConsumer \
--rpc-url <RPC_URL> \
--private-key "$FUNDED_EOA_KEY" \
--broadcast \
--constructor-args $COMPOSE_WALLET
--broadcast は必須です(forge ≥1.0 はデフォルトでドライランを行うため、成功したように見えても何もデプロイされません)。また、--constructor-args は必ず最後のフラグにしてください。これは可変個数の引数を受け取るため、--rpc-url / --private-key より前に置くと forge がそれらを取り込んでしまい、localhost に接続します。デプロイ先のアドレスは Deployed to: の行に表示されます。これを $CONTRACT_ADDRESS として取得してください。
ABI のみを抽出する(必須)。 forge build は out/RandomnessConsumer.sol/RandomnessConsumer.json に完全なアーティファクト {abi, bytecode, ...} を書き込みます。compose codegen に必要なのは ABI のみの配列 です。アーティファクト全体を渡すと警告が出ますが、codegen は「✓ complete」と出力し続けます(気付かないまま失敗し、実行時に「not a constructor」が発生します)。次のように抽出します。
jq .abi out/RandomnessConsumer.sol/RandomnessConsumer.json > src/contracts/RandomnessConsumer.json
(3)codegen を実行する。 deployContract が上記の ABI を保存しています(forge の経路では jq .abi による抽出結果が保存されています)。codegen はタスクがインポートする型付きの evm.contracts.RandomnessConsumer クラスを生成します(CLI 自身の成功メッセージでもそのように案内されます)。
goldsky compose codegen
(4) アドレスとチェーンをコードに組み込みます(ステップ 4)。(5) アプリをデプロイして、組み込み済みのタスクを稼働させます(ステップ 7)。デプロイは再デプロイではなく 1 回です。ウォレットの作成とコントラクトのデプロイは先に済ませているため、このデプロイで組み込み済みのタスクが稼働します。
ステップ 4 — コントラクトアドレスとチェーンをコードに設定する
3 か所を同期させる必要があります。行番号は時間とともに変わるため、grep のアンカーを使ってください。
compose.yaml:
- トップレベルの
name:→"<app name>"。 onchain_eventトリガー内のnetwork:→"<chosen chain in snake_case>"、contract:→"<CONTRACT_ADDRESS>"。
src/tasks/fulfill-randomness.ts と src/tasks/request-randomness.ts(両方):
const CONTRACT_ADDRESS = "0x..."→<CONTRACT_ADDRESS>。new evm.contracts.RandomnessConsumer(...)内のevm.chains.baseSepoliaの参照 →evm.chains.<chosen chain in camelCase>。
適用する前に差分を表示し、その後 Edit で適用してください。
ステップ 5 — ガス(独自にデプロイする場合)
Compose 管理のウォレットではデフォルトで sponsorGas: true が設定されており、ランタイムのバンドラーによる一括スポンサー機能は、このスキルで挙げるすべてのチェーン(Base/Base Sepolia と、forge のフォールバック先チェーン(arbitrumSepolia、arbitrum、optimismSepolia、optimism))をカバーしています。そのため、ランタイムで $COMPOSE_WALLET に資金を入れる必要はどのチェーンでもありません。ウォレットへの資金提供は一切スキップしてください。このフローで資金を入れる必要がある EOA は、Base 以外のチェーンで forge create によるデプロイを実行するものだけです(ステップ 3、分岐 B →「Base 以外のチェーン」)。その点はすでにそこで説明されています。
フォールバック先チェーンのパブリック RPC エンドポイント(<RPC_URL> の代わりに forge create/cast で使用してください):
arbitrumSepolia→https://sepolia-rollup.arbitrum.io/rpcarbitrum→https://arb1.arbitrum.io/rpcoptimismSepolia→https://sepolia.optimism.iooptimism→https://mainnet.optimism.io
ステップ 6 — 任意:新しい GitHub リポジトリに公開する
git init
git add .
if git ls-files --cached | grep -qiE '(keypair\.json|\.env|private[._-]?key|\.pem|id_rsa)'; then
echo "⚠ Secret-shaped file is staged — remove it before committing. Aborting publish."
else
git commit -m "Initial commit: Compose VRF"
gh repo create <user's repo name> --<public|private> --source=. --push
fi
ステップ 7 — Goldsky にデプロイする
goldsky compose deploy
初回のデプロイには 1~2 分かかる場合があります。出力に Deployed compose app: <app_name> と HTTP タスク URL が表示されるまで確認してください。(分岐 A ではここで初めてデプロイします。分岐 B もここで初めてデプロイします。ウォレットの作成とコントラクトのデプロイはステップ 3 で済んでいるため、この 1 回のデプロイで設定済みのタスクが稼働します。)
Step 8 — スモークテスト
デプロイ済みアプリにリクエストを送信します。推奨のトリガーは、ガス代がスポンサーされる goldsky compose writeContract です。アプリ独自の Compose スマートウォレットから requestRandomness() を呼び出すため、トークンを探し回ったり、資金を入れた EOA を用意したりする必要はありません。
goldsky compose writeContract \
--chain-id 84532 \
--to $CONTRACT_ADDRESS \
--function "requestRandomness()"
(writeContract はクラウドの Alchemy バンドラーを経由します。対応チェーンは Ethereum、Sepolia、Polygon、Polygon Amoy、Arbitrum、Arbitrum Sepolia、Optimism、Optimism Sepolia、Base、Base Sepolia(チェーン ID は 1、11155111、137、80002、42161、421614、10、11155420、8453、84532)です。この一覧にないチェーンでは No Alchemy bundler URL for chain <id> というエラーで呼び出しが失敗するため、以下の cast send を使用してください。--chain-id 84532(Base Sepolia)またはサポートされているチェーン ID を指定してください。名前付き/デフォルトのガス代スポンサー付きウォレットから送信されます。認証方法:シェルで一度 GOLDSKY_API_TOKEN=<project API key> をエクスポートする(推奨:キーが argv やシェル履歴に残りません)か、コマンドごとに -t "$GOLDSKY_PROJECT_KEY" を渡すか、有効な goldsky login セッションを使用します。優先順位は --token > GOLDSKY_API_TOKEN > ~/.goldsky/auth_token です。401 が返された場合は、Settings > API Keys を確認するよう案内されます。リクエスト ID を取得するには、オンチェーンで nextRequestId を読み取り、1 を引いてください。)
代替手段(ラベルはそのままにしてください。推奨手順では、上記のスポンサー付き writeContract を優先します):
- HTTP タスク(curl) —
request_randomnessタスクで、リクエスト → イベント → fulfill の全経路を実行します:curl -X POST \ -H "Authorization: Bearer $COMPOSE_TOKEN" \ "https://api.goldsky.com/api/admin/compose/v1/<app name>/tasks/request_randomness"
(goldsky compose callTask request_randomness '{}' はデプロイ済みアプリに直接アクセスし、goldsky login セッションまたは GOLDSKY_API_TOKEN を使って認証します。必要な場合は、上記の curl が同等の生の呼び出しです。compose start サーバーにアクセスするには、--env local を追加してください。)
- Cast(自分の資金を入れた EOA) — コントラクトを直接呼び出します:
cast send $CONTRACT_ADDRESS "requestRandomness()" \ --rpc-url <RPC_URL> \ --private-key "$PRIVATE_KEY" # from your own .env; never a pasted literal
Compose がイベントを処理するまで 10~30 秒待ち、その後ログをストリーミングします(-f は追跡表示です。指定しない場合、logs は一度だけ出力され、発火を見逃す可能性が高くなります):
goldsky compose logs -f
実行状況も確認してください(エージェントは非対話形式で実行されるため、ライブストリームの監視より runs の方が確実です):
goldsky compose runs --task fulfill_randomness --since 5m --json
fetched drand round <N> と fulfilled request <requestId> in tx <hash> が表示されるはずです。オンチェーンで確認します:
cast call $CONTRACT_ADDRESS "isFulfilled(uint256)(bool)" <requestId> --rpc-url <RPC_URL>
# → true
トラブルシューティング
- 再デプロイ後も、
compose.yamlやソースファイルへの変更が反映されません。 ローカルの.compose/バンドルキャッシュが古くなっています。rm -rf .compose/を実行してから、再デプロイしてください。 fulfillRandomnessでOnlyFulfiller()によるリバートが発生します。 オープンな0x6273AB…ではなく、許可制 コントラクト(古い0xE05Ceb…のデモなど)をアプリに指定しているか、独自コントラクトで Compose ウォレット以外のアドレスにフルフィルメントを制限しています。共有のオープンコントラクトを使用するか、コントラクトのフルフィラーに$COMPOSE_WALLETを設定してください。- イベントが発行されてもタスクが実行されません。
compose.yamlに正確なcontract:アドレスと正しいnetwork:が設定されていること、デプロイが成功していること、トリガーが有効であること(goldsky compose status)を確認してください。 insufficient funds for gasが発生します。 スポンサー対象外のチェーンでのみ発生します。$COMPOSE_WALLETに資金を入れてください。- drand の取得に失敗します。 drand のデフォルトエンドポイントは公開されています。
compose.yamlの再試行設定(最大 3 回、バックオフ)が一時的な失敗に対応します。継続して失敗する場合は、https://api.drand.sh/chains を確認してください。 error: Unknown command "deployContract". Did you mean command "deploy"?(またはwriteContractの場合も同様)が表示されます。 Compose CLI が古すぎます。goldsky compose update(またはgoldsky compose update 0.8.1)を実行し、goldsky compose --versionで0.8.1以上と表示されることを確認してから、再試行してください。deployContractが「This contract has already been deployed with this app…」というエラーで拒否されます。 同一のコントラクト、コンストラクター引数、アプリ、プロジェクトでdeployContractを再実行すると、同じ CREATE2 アドレスが再計算され、トランザクションの前に拒否されます(Deploy Block:は表示されません)。新たにデプロイするには、要素を 1 つ変更してください。たとえば、別のコンストラクター引数(RandomnessConsumerは_fulfillerを受け取ります)、変更したコントラクトソース、または別のアプリ名を指定します。新しい名前は、大文字・小文字だけ、または-/_だけが異なる名前にはできません。名前は正規化(小文字化、[-_]+->-)されるため、my-app、My_App、my__appは同一とみなされ、デプロイは 409 を返します。コンストラクター引数またはソースを変更する方法を推奨します。アプリ名を変更すると、今後のすべてのデプロイアドレスが変わり、compose deployのアプリ名とも競合するため、最も影響の大きい方法です。
してはいけないこと
- 許可制の
0xE05Ceb3E269029E3bab46E35515e8987060D1027デモをアプリに指定しないでください。既製のフルフィルメントはそこでリバートします。デプロイ不要の共有コントラクトは0x6273AB73C95Ba2233281F1eb8aa3b21D9352AD6dです。 - ユーザーが drand ネットワークの切り替えを明示的に求めない限り、
src/lib/drand.tsの drand 定数を変更しないでください。これらはチェーン固有の BLS パラメーターです。誤ると、署名検証がエラーなく失敗します。 compose.yamlのイベントシグネチャRandomnessRequested(uint256,address)を変更しないでください。コントラクトと一致している必要があります。- 共有の Base Sepolia コントラクトを本番環境のターゲットとして使用しないでください。誰でもフルフィルメントできる状態になっています。
- このアプリに
PRIVATE_KEYシークレットを追加しないでください。Compose ウォレットが実行時に署名します。Base/Base Sepolia では、Branch B のデプロイにも EOA は不要です(deployContractはガス代がスポンサーされます)。上記の forge フォールバックは Base 以外のチェーンでデプロイする場合に限り、資金のある EOA を使用します。実行時には決して使用しないでください。
関連項目
/compose— 新規またはカスタムの Compose アプリを一から構築するか、Compose について説明します。/compose-reference— マニフェスト、CLI、TaskContext API、ウォレット、ガス代のスポンサー、コード生成。/compose-doctor— 破損した Compose アプリを診断して修復します。/auth-setup—goldsky loginの手順。