From e6b43182a54d73b42ddd9b2186642234dcabb0ca Mon Sep 17 00:00:00 2001 From: RedPanda Date: Tue, 27 Jan 2026 14:05:02 +0530 Subject: [PATCH 01/30] add: external api reference implementation --- .github/workflows/ci.yml | 37 ++++++ examples/.gitignore | 2 + examples/README.md | 56 ++++++++ examples/check-balances.js | 144 +++++++++++++++++++++ examples/package.json | 17 +++ examples/read-devnet-info.js | 162 +++++++++++++++++++++++ src/commands/start/mod.rs | 58 ++++----- src/commands/start/step/mod.rs | 13 +- src/external_api/devnet_info.rs | 130 +++++++++++++++++++ src/external_api/export.rs | 219 ++++++++++++++++++++++++++++++++ src/external_api/mod.rs | 20 +++ src/lib.rs | 1 + src/paths.rs | 6 + 13 files changed, 830 insertions(+), 35 deletions(-) create mode 100644 examples/.gitignore create mode 100644 examples/README.md create mode 100644 examples/check-balances.js create mode 100644 examples/package.json create mode 100644 examples/read-devnet-info.js create mode 100644 src/external_api/devnet_info.rs create mode 100644 src/external_api/export.rs create mode 100644 src/external_api/mod.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3496da0..9c86e68b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -326,6 +326,43 @@ jobs: echo "Containers using foc-* images (running or exited):" docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' + # Verify devnet-info.json was exported successfully + - name: "CHECK: {Verify devnet-info.json exists}" + if: steps.start_cluster.outcome == 'success' + run: | + DEVNET_INFO="$HOME/.foc-devnet/state/latest/devnet-info.json" + if [ ! -f "$DEVNET_INFO" ]; then + echo "ERROR: devnet-info.json was not created at $DEVNET_INFO" + exit 1 + fi + echo "✓ devnet-info.json exists at $DEVNET_INFO" + echo "--- Contents ---" + cat "$DEVNET_INFO" + echo "" + echo "--- Schema validation ---" + VERSION=$(cat "$DEVNET_INFO" | jq -r '.version') + RUN_ID=$(cat "$DEVNET_INFO" | jq -r '.info.run_id') + echo "Schema version: $VERSION" + echo "Run ID: $RUN_ID" + if [ "$VERSION" != "1" ]; then + echo "ERROR: Unexpected schema version: $VERSION" + exit 1 + fi + if [ -z "$RUN_ID" ] || [ "$RUN_ID" == "null" ]; then + echo "ERROR: run_id is missing or null" + exit 1 + fi + echo "✓ devnet-info.json schema is valid" + + # Run the JavaScript example to verify it works + - name: "CHECK: {Verify read-devnet-info.js works}" + if: steps.start_cluster.outcome == 'success' + run: | + cd examples + npm install + node read-devnet-info.js ~/.foc-devnet/state/latest/devnet-info.json + echo "✓ read-devnet-info.js executed successfully" + # Clean shutdown - name: "EXEC: {Stop cluster}, independent" run: ./foc-devnet stop diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 00000000..504afef8 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..c4250392 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,56 @@ +# FOC DevNet Examples + +This directory contains examples demonstrating how to interact with a running FOC DevNet instance using the exported `devnet-info.json` file. + +## Files + +- `read-devnet-info.js` - JavaScript example showing how to read and use the DevNet info +- `check-balances.js` - JavaScript example demonstrating how to check user balances + +## Prerequisites + +- Node.js 18+ installed +- A running FOC DevNet instance (`foc-devnet start`) +- npm packages: `ethers` (for blockchain interaction) + +## Usage + +1. Start the DevNet: + ```bash + foc-devnet start + ``` + +2. Find the devnet-info.json file: + ```bash + # The file is located in the run directory, accessible via the latest symlink + cat ~/.foc-devnet/state/latest/devnet-info.json + ``` + +3. Run an example: + ```bash + cd examples + npm install + node read-devnet-info.js ~/.foc-devnet/state/latest/devnet-info.json + ``` + +## DevNet Info Schema (Version 1) + +The `devnet-info.json` file contains: + +```json +{ + "version": 1, + "info": { + "run_id": "...", + "start_time": "2026-01-27T...", + "startup_duration": "539.04s", + "users": [...], + "contracts": {...}, + "lotus": {...}, + "lotus_miner": {...}, + "curio_providers": [...] + } +} +``` + +See `read-devnet-info.js` for detailed usage of each field. diff --git a/examples/check-balances.js b/examples/check-balances.js new file mode 100644 index 00000000..616e45f1 --- /dev/null +++ b/examples/check-balances.js @@ -0,0 +1,144 @@ +/** + * FOC DevNet Balance Checker + * + * This example demonstrates how to use ethers.js to check balances + * of user accounts on the DevNet using the exported devnet-info.json. + * + * Usage: + * node check-balances.js [path-to-devnet-info.json] + * + * If no path is provided, defaults to ~/.foc-devnet/state/latest/devnet-info.json + */ + +import { readFileSync, existsSync } from "fs"; +import { homedir } from "os"; +import { join } from "path"; +import { ethers } from "ethers"; + +// ERC20 ABI (minimal for balanceOf) +const ERC20_ABI = [ + "function balanceOf(address owner) view returns (uint256)", + "function decimals() view returns (uint8)", + "function symbol() view returns (string)", +]; + +/** + * Load the devnet-info.json file. + * @param {string} filePath - Path to the devnet-info.json file + * @returns {object} The parsed DevNet info + */ +function loadDevnetInfo(filePath) { + if (!existsSync(filePath)) { + throw new Error(`DevNet info file not found: ${filePath}`); + } + const content = readFileSync(filePath, "utf8"); + return JSON.parse(content); +} + +/** + * Format wei to ether with specified decimals. + * @param {bigint} wei - Amount in wei + * @param {number} decimals - Decimal places + * @returns {string} Formatted amount + */ +function formatBalance(wei, decimals = 18) { + return ethers.formatUnits(wei, decimals); +} + +/** + * Check native FIL balance for an address. + * @param {ethers.Provider} provider - Ethers provider + * @param {string} address - Address to check + * @returns {Promise} Balance in FIL + */ +async function checkNativeBalance(provider, address) { + const balance = await provider.getBalance(address); + return formatBalance(balance); +} + +/** + * Check ERC20 token balance for an address. + * @param {ethers.Provider} provider - Ethers provider + * @param {string} tokenAddress - Token contract address + * @param {string} userAddress - User address to check + * @returns {Promise<{balance: string, symbol: string}>} Balance and symbol + */ +async function checkTokenBalance(provider, tokenAddress, userAddress) { + const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider); + const [balance, decimals, symbol] = await Promise.all([ + contract.balanceOf(userAddress), + contract.decimals(), + contract.symbol(), + ]); + return { + balance: formatBalance(balance, decimals), + symbol, + }; +} + +// Main execution +async function main() { + // Determine file path + const defaultPath = join( + homedir(), + ".foc-devnet", + "state", + "latest", + "devnet-info.json" + ); + const filePath = process.argv[2] || defaultPath; + + console.log(`Loading DevNet info from: ${filePath}\n`); + + try { + const { info } = loadDevnetInfo(filePath); + + // Connect to the Lotus RPC + const provider = new ethers.JsonRpcProvider(info.lotus.host_rpc_url); + console.log(`Connected to: ${info.lotus.host_rpc_url}`); + + // Check if network is accessible + const blockNumber = await provider.getBlockNumber(); + console.log(`Current block: ${blockNumber}\n`); + + console.log("═══════════════════════════════════════════════════════════"); + console.log(" Account Balances"); + console.log("═══════════════════════════════════════════════════════════\n"); + + // Check balances for all users + for (const user of info.users) { + console.log(`${user.name} (${user.evm_addr}):`); + + // Check native FIL balance + const filBalance = await checkNativeBalance(provider, user.evm_addr); + console.log(` Native FIL: ${filBalance} tFIL`); + + // Check MockUSDFC balance + if (info.contracts.mockusdfc_addr) { + try { + const { balance, symbol } = await checkTokenBalance( + provider, + info.contracts.mockusdfc_addr, + user.evm_addr + ); + console.log(` ${symbol}: ${balance}`); + } catch (e) { + console.log(` MockUSDFC: Error - ${e.message}`); + } + } + console.log(); + } + + console.log("═══════════════════════════════════════════════════════════\n"); + } catch (error) { + console.error(`Error: ${error.message}`); + if (error.code === "ECONNREFUSED") { + console.error( + "Could not connect to the DevNet. Make sure foc-devnet is running." + ); + } + process.exit(1); + } +} + +main(); diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 00000000..0704f46f --- /dev/null +++ b/examples/package.json @@ -0,0 +1,17 @@ +{ + "name": "foc-devnet-examples", + "version": "1.0.0", + "description": "Examples for interacting with FOC DevNet", + "type": "module", + "scripts": { + "read-info": "node read-devnet-info.js", + "check-balances": "node check-balances.js" + }, + "dependencies": { + "ethers": "^6.0.0" + }, + "devDependencies": {}, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/examples/read-devnet-info.js b/examples/read-devnet-info.js new file mode 100644 index 00000000..886be9e0 --- /dev/null +++ b/examples/read-devnet-info.js @@ -0,0 +1,162 @@ +/** + * FOC DevNet Info Reader + * + * This example demonstrates how to read and use the devnet-info.json file + * exported by foc-devnet after a successful start. + * + * Usage: + * node read-devnet-info.js [path-to-devnet-info.json] + * + * If no path is provided, defaults to ~/.foc-devnet/state/latest/devnet-info.json + */ + +import { readFileSync, existsSync } from "fs"; +import { homedir } from "os"; +import { join } from "path"; + +const SCHEMA_VERSION = 1; + +/** + * Load and validate the devnet-info.json file. + * @param {string} filePath - Path to the devnet-info.json file + * @returns {object} The parsed DevNet info + */ +function loadDevnetInfo(filePath) { + if (!existsSync(filePath)) { + throw new Error(`DevNet info file not found: ${filePath}`); + } + + const content = readFileSync(filePath, "utf8"); + const data = JSON.parse(content); + + // Validate schema version + if (data.version !== SCHEMA_VERSION) { + console.warn( + `Warning: Expected schema version ${SCHEMA_VERSION}, got ${data.version}` + ); + } + + return data; +} + +/** + * Print summary of the DevNet state. + * @param {object} info - The DevnetInfoV1 object + */ +function printSummary(info) { + console.log("\n═══════════════════════════════════════════════════════════"); + console.log(" FOC DevNet Summary"); + console.log("═══════════════════════════════════════════════════════════\n"); + + console.log(`Run ID: ${info.run_id}`); + console.log(`Start Time: ${info.start_time}`); + console.log(`Startup Duration: ${info.startup_duration}`); +} + +/** + * Print Lotus node information. + * @param {object} lotus - The LotusInfo object + */ +function printLotusInfo(lotus) { + console.log("\n─── Lotus Node ───────────────────────────────────────────\n"); + console.log(`RPC URL: ${lotus.host_rpc_url}`); + console.log(`Container: ${lotus.container_name}`); + console.log(`Container ID: ${lotus.container_id.substring(0, 12)}...`); +} + +/** + * Print deployed contract addresses. + * @param {object} contracts - The ContractsInfo object + */ +function printContracts(contracts) { + console.log("\n─── Deployed Contracts ───────────────────────────────────\n"); + console.log(`MockUSDFC: ${contracts.mockusdfc_addr}`); + console.log(`Multicall3: ${contracts.multicall3_addr}`); + console.log(`FWSS Proxy: ${contracts.fwss_service_proxy_addr}`); + console.log(`PDP Verifier Proxy: ${contracts.pdp_verifier_proxy_addr}`); + console.log(`Service Provider Registry: ${contracts.service_provider_registry_proxy_addr}`); + console.log(`FilecoinPay V1: ${contracts.filecoin_pay_v1_addr}`); +} + +/** + * Print user account information. + * @param {Array} users - Array of UserInfo objects + */ +function printUsers(users) { + console.log("\n─── User Accounts ────────────────────────────────────────\n"); + + for (const user of users) { + console.log(`${user.name}:`); + console.log(` EVM Address: ${user.evm_addr}`); + console.log(` Native Address: ${user.native_addr}`); + console.log(` tFIL Balance: ${user.native_balance_tfil}`); + console.log(` USDFC Balance: ${formatTokenBalance(user.mockusdfc_balance, 18)}`); + console.log(` Private Key: ${user.private_key_hex.substring(0, 8)}...`); + console.log(); + } +} + +/** + * Print Curio service provider information. + * @param {Array} providers - Array of CurioInfo objects + */ +function printCurioProviders(providers) { + console.log("\n─── Curio Service Providers ──────────────────────────────\n"); + + for (const provider of providers) { + console.log(`Provider ${provider.provider_id}:`); + console.log(` ETH Address: ${provider.eth_addr}`); + console.log(` PDP Service URL: ${provider.pdp_service_url}`); + console.log(` YugabyteDB:`); + console.log(` Web UI: ${provider.yugabyte.web_ui_url}`); + console.log(` YSQL Port: ${provider.yugabyte.ysql_port}`); + console.log(); + } +} + +/** + * Format a token balance from wei to human-readable form. + * @param {string} balance - Balance in wei (as string) + * @param {number} decimals - Token decimals + * @returns {string} Formatted balance + */ +function formatTokenBalance(balance, decimals) { + const balanceBigInt = BigInt(balance); + const divisor = BigInt(10 ** decimals); + const whole = balanceBigInt / divisor; + const fraction = balanceBigInt % divisor; + return `${whole.toString()}.${fraction.toString().padStart(decimals, "0").substring(0, 4)}`; +} + +// Main execution +function main() { + // Determine file path + const defaultPath = join( + homedir(), + ".foc-devnet", + "state", + "latest", + "devnet-info.json" + ); + const filePath = process.argv[2] || defaultPath; + + console.log(`Loading DevNet info from: ${filePath}`); + + try { + const { version, info } = loadDevnetInfo(filePath); + console.log(`Schema version: ${version}`); + + printSummary(info); + printLotusInfo(info.lotus); + printContracts(info.contracts); + printUsers(info.users); + printCurioProviders(info.curio_providers); + + console.log("═══════════════════════════════════════════════════════════\n"); + } catch (error) { + console.error(`Error: ${error.message}`); + process.exit(1); + } +} + +main(); diff --git a/src/commands/start/mod.rs b/src/commands/start/mod.rs index fb09d895..757987a0 100644 --- a/src/commands/start/mod.rs +++ b/src/commands/start/mod.rs @@ -331,7 +331,7 @@ fn execute_cluster_steps( parallel: bool, portainer_port: u16, notest: bool, -) -> Result<(), Box> { +) -> Result> { // Ensure genesis prerequisites are ready (one-time setup, needs config for sector count) ensure_genesis_prerequisites(config.active_pdp_sp_count, run_id)?; @@ -352,6 +352,16 @@ fn execute_cluster_steps( config.active_pdp_sp_count, config.approved_pdp_sp_count ); + let step_config = step::StepExecutionConfig { + run_id: run_id.to_string(), + run_dir: run_dir.to_path_buf(), + port_start: config.port_range_start, + port_count: config.port_range_count, + portainer_port: Some(portainer_port), + active_pdp_sp_count: config.active_pdp_sp_count, + approved_pdp_sp_count: config.approved_pdp_sp_count, + }; + if parallel { info!("Execution mode: PARALLEL (experimental)"); let step_epochs = create_step_epochs(volumes_dir, run_dir, config, notest); @@ -362,37 +372,18 @@ fn execute_cluster_steps( .map(|epoch| epoch.iter().map(|s| s.as_ref()).collect()) .collect(); - execute_steps_parallel( - epoch_refs, - step::StepExecutionConfig { - run_id: run_id.to_string(), - run_dir: run_dir.to_path_buf(), - port_start: config.port_range_start, - port_count: config.port_range_count, - portainer_port: Some(portainer_port), - active_pdp_sp_count: config.active_pdp_sp_count, - approved_pdp_sp_count: config.approved_pdp_sp_count, - }, - )?; + let context = execute_steps_parallel(epoch_refs, step_config)?; + Ok(context) } else { info!("Execution mode: SEQUENTIAL"); let steps = create_steps(volumes_dir, run_dir, config, notest); - execute_steps( + let context = execute_steps( steps.iter().map(|s| s.as_ref()).collect::>(), - step::StepExecutionConfig { - run_id: run_id.to_string(), - run_dir: run_dir.to_path_buf(), - port_start: config.port_range_start, - port_count: config.port_range_count, - portainer_port: Some(portainer_port), - active_pdp_sp_count: config.active_pdp_sp_count, - approved_pdp_sp_count: config.approved_pdp_sp_count, - }, + step_config, )?; + Ok(context) } - - Ok(()) } /// Start the local Filecoin network cluster. @@ -447,11 +438,18 @@ pub fn start_cluster( warn!("Post-start teardown encountered an error: {}", e); } - // Propagate original execution result - exec_result?; - - info!("Cluster started successfully!"); - Ok(()) + // Export devnet info if steps succeeded + match exec_result { + Ok(context) => { + // Export the devnet info JSON for external consumers + if let Err(e) = crate::external_api::export_devnet_info(&context) { + warn!("Failed to export devnet info: {}", e); + } + info!("Cluster started successfully!"); + Ok(()) + } + Err(e) => Err(e), + } } /// Finalize the start attempt by collecting logs, cleaning dead containers, and writing status. diff --git a/src/commands/start/step/mod.rs b/src/commands/start/step/mod.rs index cbf97c57..20a39618 100644 --- a/src/commands/start/step/mod.rs +++ b/src/commands/start/step/mod.rs @@ -323,7 +323,7 @@ pub struct StepExecutionConfig { pub fn execute_steps( steps: Vec<&dyn Step>, config: StepExecutionConfig, -) -> Result<(), Box> { +) -> Result> { // Create port allocator and verify all ports are available let mut port_allocator = PortAllocator::new(config.port_start, config.port_count)?; @@ -391,7 +391,7 @@ pub fn execute_steps( .collect(); context.set_multi(timing_items); - Ok(()) + Ok(context) } /// Execute steps organized in parallel epochs @@ -410,11 +410,11 @@ pub fn execute_steps( /// /// # Returns /// -/// Returns Ok(()) if all steps in all epochs complete successfully, or an error if any step fails. +/// Returns Ok(SetupContext) if all steps in all epochs complete successfully, or an error if any step fails. pub fn execute_steps_parallel( step_epochs: Vec>, config: StepExecutionConfig, -) -> Result<(), Box> { +) -> Result> { // Create port allocator and verify all ports are available let mut port_allocator = PortAllocator::new(config.port_start, config.port_count)?; @@ -487,7 +487,10 @@ pub fn execute_steps_parallel( warn!("Failed to save step context: {}", e); } - Ok(()) + // Unwrap Arc to return owned SetupContext + let context = Arc::try_unwrap(context).map_err(|_| "Failed to unwrap Arc")?; + + Ok(context) } /// Execute a single epoch of steps (either sequentially or in parallel) diff --git a/src/external_api/devnet_info.rs b/src/external_api/devnet_info.rs new file mode 100644 index 00000000..49c626e6 --- /dev/null +++ b/src/external_api/devnet_info.rs @@ -0,0 +1,130 @@ +//! DevNet information data structures. +//! +//! Contains the versioned schema for exporting DevNet state to external consumers. + +use serde::{Deserialize, Serialize}; + +/// Versioned wrapper for DevNet information. +/// +/// This struct provides a stable interface for external consumers. The `version` +/// field indicates the schema version, allowing consumers to handle migrations. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VersionedDevnetInfo { + /// Schema version number. Increment when breaking changes are made. + pub version: u32, + /// The actual DevNet information payload. + pub info: DevnetInfoV1, +} + +/// DevNet information schema version 1. +/// +/// Contains all relevant information about a running DevNet instance. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DevnetInfoV1 { + /// Unique identifier for this run (e.g., "26jan20-1622_GoofyNubs") + pub run_id: String, + /// ISO 8601 formatted start time + pub start_time: String, + /// Time taken to start the system (e.g., "539.04s") + pub startup_duration: String, + /// User accounts available for testing + pub users: Vec, + /// Deployed contract addresses + pub contracts: ContractsInfo, + /// Lotus node information + pub lotus: LotusInfo, + /// Lotus miner information + pub lotus_miner: LotusMinerInfo, + /// Curio service providers + pub curio_providers: Vec, +} + +/// Information about a user account. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserInfo { + /// User identifier (e.g., "USER_1") + pub name: String, + /// Ethereum-compatible address (0x...) + pub evm_addr: String, + /// Native Filecoin address (t410f...) + pub native_addr: String, + /// Balance in tFIL + pub native_balance_tfil: String, + /// Balance in MockUSDFC tokens + pub mockusdfc_balance: String, + /// Private key in hex format (without 0x prefix) + pub private_key_hex: String, +} + +/// Deployed contract addresses. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractsInfo { + /// Multicall3 contract address + pub multicall3_addr: String, + /// MockUSDFC token contract address + pub mockusdfc_addr: String, + /// Filecoin Warm Storage Service proxy address + pub fwss_service_proxy_addr: String, + /// Filecoin Warm Storage Service state view address + pub fwss_state_view_addr: String, + /// Filecoin Warm Storage Service implementation address + pub fwss_impl_addr: String, + /// PDP Verifier proxy address + pub pdp_verifier_proxy_addr: String, + /// PDP Verifier implementation address + pub pdp_verifier_impl_addr: String, + /// Service Provider Registry proxy address + pub service_provider_registry_proxy_addr: String, + /// Service Provider Registry implementation address + pub service_provider_registry_impl_addr: String, + /// FilecoinPay V1 contract address + pub filecoin_pay_v1_addr: String, +} + +/// Lotus node information. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LotusInfo { + /// RPC URL exposed to the host + pub host_rpc_url: String, + /// Docker container ID + pub container_id: String, + /// Docker container name + pub container_name: String, +} + +/// Lotus miner information. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LotusMinerInfo { + /// Docker container ID + pub container_id: String, + /// Docker container name + pub container_name: String, + /// API port + pub api_port: u16, +} + +/// Curio service provider information. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CurioInfo { + /// Provider ID (1-indexed) + pub provider_id: u32, + /// Ethereum address of the provider + pub eth_addr: String, + /// Native Filecoin address + pub native_addr: String, + /// PDP service URL accessible from host + pub pdp_service_url: String, + /// YugabyteDB information for this provider + pub yugabyte: YugabyteInfo, +} + +/// YugabyteDB connection information. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct YugabyteInfo { + /// Web UI URL exposed to host + pub web_ui_url: String, + /// Master RPC port + pub master_rpc_port: u16, + /// YSQL port for Postgres-compatible connections + pub ysql_port: u16, +} diff --git a/src/external_api/export.rs b/src/external_api/export.rs new file mode 100644 index 00000000..28d2a8d8 --- /dev/null +++ b/src/external_api/export.rs @@ -0,0 +1,219 @@ +//! Export DevNet information from SetupContext. +//! +//! This module extracts data from the SetupContext and produces a +//! VersionedDevnetInfo structure that can be serialized to JSON. + +use std::path::Path; + +use chrono::Utc; + +use crate::commands::start::step::SetupContext; +use crate::crypto::derive_ethereum_key; +use crate::crypto::mnemonic::load_mnemonic; +use crate::external_api::{ + ContractsInfo, CurioInfo, DevnetInfoV1, LotusInfo, LotusMinerInfo, UserInfo, + VersionedDevnetInfo, YugabyteInfo, DEVNET_INFO_FILENAME, DEVNET_INFO_SCHEMA_VERSION, +}; + +/// Export DevNet information to a JSON file. +/// +/// Extracts all relevant information from the SetupContext and writes +/// it to `devnet-info.json` in the run directory. +pub fn export_devnet_info(context: &SetupContext) -> Result<(), Box> { + let info = build_devnet_info(context)?; + let versioned = VersionedDevnetInfo { + version: DEVNET_INFO_SCHEMA_VERSION, + info, + }; + + let output_path = context.run_dir().join(DEVNET_INFO_FILENAME); + write_json_file(&output_path, &versioned)?; + + tracing::info!("Exported DevNet info to: {}", output_path.display()); + Ok(()) +} + +/// Build DevnetInfoV1 from SetupContext. +fn build_devnet_info(ctx: &SetupContext) -> Result> { + Ok(DevnetInfoV1 { + run_id: ctx.run_id().to_string(), + start_time: Utc::now().to_rfc3339(), + startup_duration: ctx + .get("step_timing_total_execution_time") + .unwrap_or_else(|| "unknown".to_string()), + users: build_users(ctx)?, + contracts: build_contracts(ctx), + lotus: build_lotus_info(ctx), + lotus_miner: build_lotus_miner_info(ctx), + curio_providers: build_curio_providers(ctx), + }) +} + +/// Build user information from context and mnemonic. +fn build_users(ctx: &SetupContext) -> Result, Box> { + let mnemonic = load_mnemonic()?; + let seed = mnemonic.to_seed(""); + + let mut users = Vec::new(); + for i in 1..=3 { + let name = format!("USER_{}", i); + let user = build_single_user(ctx, &name, &seed)?; + users.push(user); + } + Ok(users) +} + +/// Build a single user's info. +fn build_single_user( + ctx: &SetupContext, + name: &str, + seed: &[u8; 64], +) -> Result> { + let key_name = name.to_uppercase(); + let derived = derive_ethereum_key(seed, &key_name)?; + + let evm_addr = ctx + .get(&format!("{}_eth_address", name.to_lowercase())) + .or_else(|| derived.eth_address.clone()) + .unwrap_or_default(); + + let native_addr = ctx + .get(&format!("{}_address", name.to_lowercase())) + .unwrap_or_else(|| derived.native_address.clone()); + + // TODO: Query actual balances from chain if needed + Ok(UserInfo { + name: name.to_string(), + evm_addr, + native_addr, + native_balance_tfil: "1000".to_string(), // Default funding amount + mockusdfc_balance: "100000000000000000000000".to_string(), // 100,000 USDFC + private_key_hex: derived.private_key, + }) +} + +/// Build contracts info from context. +fn build_contracts(ctx: &SetupContext) -> ContractsInfo { + ContractsInfo { + multicall3_addr: ctx.get("multicall3_address").unwrap_or_default(), + mockusdfc_addr: ctx.get("mockusdfc_contract_address").unwrap_or_default(), + fwss_service_proxy_addr: ctx + .get("foc_contract_filecoin_warm_storage_service_proxy") + .unwrap_or_default(), + fwss_state_view_addr: ctx + .get("foc_contract_filecoin_warm_storage_service_state_view") + .unwrap_or_default(), + fwss_impl_addr: ctx + .get("foc_contract_filecoin_warm_storage_service_implementation") + .unwrap_or_default(), + pdp_verifier_proxy_addr: ctx + .get("foc_contract_p_d_p_verifier_proxy") + .unwrap_or_default(), + pdp_verifier_impl_addr: ctx + .get("foc_contract_p_d_p_verifier_implementation") + .unwrap_or_default(), + service_provider_registry_proxy_addr: ctx + .get("foc_contract_service_provider_registry_proxy") + .unwrap_or_default(), + service_provider_registry_impl_addr: ctx + .get("foc_contract_service_provider_registry_implementation") + .unwrap_or_default(), + filecoin_pay_v1_addr: ctx + .get("foc_contract_filecoin_pay_v1_contract") + .unwrap_or_default(), + } +} + +/// Build Lotus node info from context. +fn build_lotus_info(ctx: &SetupContext) -> LotusInfo { + let api_port = ctx + .get("lotus_api_port") + .unwrap_or_else(|| "1234".to_string()); + LotusInfo { + host_rpc_url: format!("http://localhost:{}/rpc/v1", api_port), + container_id: ctx.get("lotus_container_id").unwrap_or_default(), + container_name: ctx.get("lotus_container_name").unwrap_or_default(), + } +} + +/// Build Lotus miner info from context. +fn build_lotus_miner_info(ctx: &SetupContext) -> LotusMinerInfo { + let api_port: u16 = ctx + .get("lotus_miner_api_port") + .and_then(|p| p.parse().ok()) + .unwrap_or(2345); + + LotusMinerInfo { + container_id: ctx.get("lotus_miner_container_id").unwrap_or_default(), + container_name: ctx.get("lotus_miner_container_name").unwrap_or_default(), + api_port, + } +} + +/// Build Curio providers info from context. +fn build_curio_providers(ctx: &SetupContext) -> Vec { + let active_count: usize = ctx + .get("active_pdp_sp_count") + .and_then(|s| s.parse().ok()) + .unwrap_or(1); + + (1..=active_count) + .filter_map(|id| build_single_curio_provider(ctx, id as u32)) + .collect() +} + +/// Build a single Curio provider's info. +fn build_single_curio_provider(ctx: &SetupContext, provider_id: u32) -> Option { + let eth_addr = ctx.get(&format!("pdp_sp_{}_eth_address", provider_id))?; + let native_addr = ctx + .get(&format!("pdp_sp_{}_address", provider_id)) + .unwrap_or_default(); + let pdp_port: u16 = ctx + .get(&format!("curio_sp_{}_pdp_port", provider_id)) + .and_then(|p| p.parse().ok()) + .unwrap_or(4702); + + let yugabyte = build_yugabyte_info(ctx, provider_id); + + Some(CurioInfo { + provider_id, + eth_addr, + native_addr, + pdp_service_url: format!("http://localhost:{}", pdp_port), + yugabyte, + }) +} + +/// Build YugabyteDB info for a provider. +fn build_yugabyte_info(ctx: &SetupContext, provider_id: u32) -> YugabyteInfo { + let web_ui_port: u16 = ctx + .get(&format!("yugabyte_{}_web_ui_port", provider_id)) + .and_then(|p| p.parse().ok()) + .unwrap_or(15433); + + let master_rpc_port: u16 = ctx + .get(&format!("yugabyte_{}_master_rpc_port", provider_id)) + .and_then(|p| p.parse().ok()) + .unwrap_or(7100); + + let ysql_port: u16 = ctx + .get(&format!("yugabyte_{}_ysql_port", provider_id)) + .and_then(|p| p.parse().ok()) + .unwrap_or(5433); + + YugabyteInfo { + web_ui_url: format!("http://localhost:{}", web_ui_port), + master_rpc_port, + ysql_port, + } +} + +/// Write a serializable struct to a JSON file. +fn write_json_file( + path: &Path, + data: &T, +) -> Result<(), Box> { + let json = serde_json::to_string_pretty(data)?; + std::fs::write(path, json)?; + Ok(()) +} diff --git a/src/external_api/mod.rs b/src/external_api/mod.rs new file mode 100644 index 00000000..8dcb8259 --- /dev/null +++ b/src/external_api/mod.rs @@ -0,0 +1,20 @@ +//! External API module for DevNet information export. +//! +//! This module provides versioned, schema-based JSON output that can be consumed +//! by external tools and scripts (e.g., JavaScript, Python) to interact with +//! the running DevNet. + +mod devnet_info; +mod export; + +pub use devnet_info::{ + ContractsInfo, CurioInfo, DevnetInfoV1, LotusInfo, LotusMinerInfo, UserInfo, + VersionedDevnetInfo, YugabyteInfo, +}; +pub use export::export_devnet_info; + +/// Current schema version for DevNet info export. +pub const DEVNET_INFO_SCHEMA_VERSION: u32 = 1; + +/// Output filename for the DevNet info JSON. +pub const DEVNET_INFO_FILENAME: &str = "devnet-info.json"; diff --git a/src/lib.rs b/src/lib.rs index 1a603c4b..781ab041 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod constants; pub mod crypto; pub mod docker; pub mod embedded_assets; +pub mod external_api; pub mod logger; pub mod paths; pub mod poison; diff --git a/src/paths.rs b/src/paths.rs index 46f6a542..c3526532 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -78,6 +78,12 @@ pub fn step_context_file(run_id: &str) -> PathBuf { foc_devnet_run_dir(run_id).join("step_context.json") } +/// Returns the path to the devnet info file for a specific run +/// This is the versioned, stable schema JSON file for external consumers. +pub fn devnet_info_file(run_id: &str) -> PathBuf { + foc_devnet_run_dir(run_id).join("devnet-info.json") +} + /// Returns the path to the PDP_SP_X provider ID file for a specific run pub fn pdp_sp_provider_id_file(run_id: &str, sp_idx: usize) -> PathBuf { foc_devnet_run_dir(run_id) From 8770c76d8faadfb67073b40e12b3b0665cb21174 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Tue, 27 Jan 2026 16:16:56 +0530 Subject: [PATCH 02/30] fix: issues --- examples/read-devnet-info.js | 30 +++++++++++++++++--------- src/commands/start/mod.rs | 10 ++++++++- src/external_api/devnet_info.rs | 10 +++++++-- src/external_api/export.rs | 38 +++++++++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/examples/read-devnet-info.js b/examples/read-devnet-info.js index 886be9e0..b591a44d 100644 --- a/examples/read-devnet-info.js +++ b/examples/read-devnet-info.js @@ -76,6 +76,7 @@ function printContracts(contracts) { console.log(`PDP Verifier Proxy: ${contracts.pdp_verifier_proxy_addr}`); console.log(`Service Provider Registry: ${contracts.service_provider_registry_proxy_addr}`); console.log(`FilecoinPay V1: ${contracts.filecoin_pay_v1_addr}`); + console.log(`Endorsements: ${contracts.endorsements_addr}`); } /** @@ -90,23 +91,26 @@ function printUsers(users) { console.log(` EVM Address: ${user.evm_addr}`); console.log(` Native Address: ${user.native_addr}`); console.log(` tFIL Balance: ${user.native_balance_tfil}`); - console.log(` USDFC Balance: ${formatTokenBalance(user.mockusdfc_balance, 18)}`); - console.log(` Private Key: ${user.private_key_hex.substring(0, 8)}...`); + console.log(` USDFC Balance: ${user.mockusdfc_balance}`); + console.log(` Private Key: ${user.private_key_hex.substring(0, 10)}...`); + console.log(); console.log(); } } /** - * Print Curio service provider information. + * Print PDP service provider information. * @param {Array} providers - Array of CurioInfo objects */ function printCurioProviders(providers) { - console.log("\n─── Curio Service Providers ──────────────────────────────\n"); + console.log("\n─── PDP Service Providers ────────────────────────────────\n"); for (const provider of providers) { console.log(`Provider ${provider.provider_id}:`); console.log(` ETH Address: ${provider.eth_addr}`); console.log(` PDP Service URL: ${provider.pdp_service_url}`); + console.log(` Container: ${provider.container_name}`); + console.log(` Container ID: ${provider.container_id.substring(0, 12)}...`); console.log(` YugabyteDB:`); console.log(` Web UI: ${provider.yugabyte.web_ui_url}`); console.log(` YSQL Port: ${provider.yugabyte.ysql_port}`); @@ -121,11 +125,17 @@ function printCurioProviders(providers) { * @returns {string} Formatted balance */ function formatTokenBalance(balance, decimals) { - const balanceBigInt = BigInt(balance); - const divisor = BigInt(10 ** decimals); - const whole = balanceBigInt / divisor; - const fraction = balanceBigInt % divisor; - return `${whole.toString()}.${fraction.toString().padStart(decimals, "0").substring(0, 4)}`; + try { + const balanceBigInt = BigInt(balance); + const divisor = BigInt(10 ** decimals); + const whole = balanceBigInt / divisor; + const fraction = balanceBigInt % divisor; + const fractionStr = fraction.toString().padStart(decimals, "0"); + return `${whole}.${fractionStr.substring(0, 4)}`; + } catch (e) { + // If balance is already formatted, return as-is + return balance; + } } // Main execution @@ -150,7 +160,7 @@ function main() { printLotusInfo(info.lotus); printContracts(info.contracts); printUsers(info.users); - printCurioProviders(info.curio_providers); + printCurioProviders(info.pdp_sps); console.log("═══════════════════════════════════════════════════════════\n"); } catch (error) { diff --git a/src/commands/start/mod.rs b/src/commands/start/mod.rs index 757987a0..9da5a9f5 100644 --- a/src/commands/start/mod.rs +++ b/src/commands/start/mod.rs @@ -444,11 +444,19 @@ pub fn start_cluster( // Export the devnet info JSON for external consumers if let Err(e) = crate::external_api::export_devnet_info(&context) { warn!("Failed to export devnet info: {}", e); + } else { + info!( + "✓ DevNet info exported to: {}", + context.run_dir().join("devnet-info.json").display() + ); } info!("Cluster started successfully!"); Ok(()) } - Err(e) => Err(e), + Err(e) => { + warn!("Cluster startup failed, devnet-info.json not exported"); + Err(e) + } } } diff --git a/src/external_api/devnet_info.rs b/src/external_api/devnet_info.rs index 49c626e6..7e01c540 100644 --- a/src/external_api/devnet_info.rs +++ b/src/external_api/devnet_info.rs @@ -35,8 +35,8 @@ pub struct DevnetInfoV1 { pub lotus: LotusInfo, /// Lotus miner information pub lotus_miner: LotusMinerInfo, - /// Curio service providers - pub curio_providers: Vec, + /// PDP service providers + pub pdp_sps: Vec, } /// Information about a user account. @@ -79,6 +79,8 @@ pub struct ContractsInfo { pub service_provider_registry_impl_addr: String, /// FilecoinPay V1 contract address pub filecoin_pay_v1_addr: String, + /// Endorsements contract address + pub endorsements_addr: String, } /// Lotus node information. @@ -114,6 +116,10 @@ pub struct CurioInfo { pub native_addr: String, /// PDP service URL accessible from host pub pdp_service_url: String, + /// Docker container ID + pub container_id: String, + /// Docker container name + pub container_name: String, /// YugabyteDB information for this provider pub yugabyte: YugabyteInfo, } diff --git a/src/external_api/export.rs b/src/external_api/export.rs index 28d2a8d8..ac3ceae0 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -45,7 +45,7 @@ fn build_devnet_info(ctx: &SetupContext) -> Result String { + if wei.len() <= 18 { + // Less than 1 token + let padded = format!("{:0>18}", wei); + format!("0.{}", padded) + } else { + let split_point = wei.len() - 18; + let whole = &wei[..split_point]; + let fraction = &wei[split_point..]; + format!("{}.{}", whole, fraction) + } +} + /// Build contracts info from context. fn build_contracts(ctx: &SetupContext) -> ContractsInfo { ContractsInfo { @@ -121,6 +139,9 @@ fn build_contracts(ctx: &SetupContext) -> ContractsInfo { filecoin_pay_v1_addr: ctx .get("foc_contract_filecoin_pay_v1_contract") .unwrap_or_default(), + endorsements_addr: ctx + .get("foc_contract_endorsements") + .unwrap_or_default(), } } @@ -173,6 +194,13 @@ fn build_single_curio_provider(ctx: &SetupContext, provider_id: u32) -> Option Option Date: Tue, 27 Jan 2026 16:43:50 +0530 Subject: [PATCH 03/30] fix: CI issues --- .github/workflows/ci.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c86e68b..ca2ee378 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -330,9 +330,19 @@ jobs: - name: "CHECK: {Verify devnet-info.json exists}" if: steps.start_cluster.outcome == 'success' run: | - DEVNET_INFO="$HOME/.foc-devnet/state/latest/devnet-info.json" + echo "Finding latest run directory..." + RUN_DIR=$(ls -td "$HOME/.foc-devnet/run"/*/ 2>/dev/null | head -1) + if [ -z "$RUN_DIR" ]; then + echo "ERROR: No run directory found" + exit 1 + fi + echo "Latest run directory: $RUN_DIR" + + DEVNET_INFO="${RUN_DIR}devnet-info.json" if [ ! -f "$DEVNET_INFO" ]; then echo "ERROR: devnet-info.json was not created at $DEVNET_INFO" + echo "Contents of run directory:" + ls -la "$RUN_DIR" exit 1 fi echo "✓ devnet-info.json exists at $DEVNET_INFO" @@ -358,9 +368,13 @@ jobs: - name: "CHECK: {Verify read-devnet-info.js works}" if: steps.start_cluster.outcome == 'success' run: | + # Find the latest run directory + RUN_DIR=$(ls -td "$HOME/.foc-devnet/run"/*/ 2>/dev/null | head -1) + DEVNET_INFO="${RUN_DIR}devnet-info.json" + cd examples npm install - node read-devnet-info.js ~/.foc-devnet/state/latest/devnet-info.json + node read-devnet-info.js "$DEVNET_INFO" echo "✓ read-devnet-info.js executed successfully" # Clean shutdown From 55872bc08f95ee8ef43855bbc524b29cd8fe9ecd Mon Sep 17 00:00:00 2001 From: RedPanda Date: Tue, 27 Jan 2026 16:53:55 +0530 Subject: [PATCH 04/30] makehappy: fmt --- src/external_api/export.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/external_api/export.rs b/src/external_api/export.rs index ac3ceae0..720f1182 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -139,9 +139,7 @@ fn build_contracts(ctx: &SetupContext) -> ContractsInfo { filecoin_pay_v1_addr: ctx .get("foc_contract_filecoin_pay_v1_contract") .unwrap_or_default(), - endorsements_addr: ctx - .get("foc_contract_endorsements") - .unwrap_or_default(), + endorsements_addr: ctx.get("foc_contract_endorsements").unwrap_or_default(), } } From 4385d00718f48a792252d6de6705c05e996b8568 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Tue, 27 Jan 2026 17:17:50 +0530 Subject: [PATCH 05/30] fixes: CI --- .github/workflows/ci.yml | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca2ee378..c1630737 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -330,19 +330,15 @@ jobs: - name: "CHECK: {Verify devnet-info.json exists}" if: steps.start_cluster.outcome == 'success' run: | - echo "Finding latest run directory..." - RUN_DIR=$(ls -td "$HOME/.foc-devnet/run"/*/ 2>/dev/null | head -1) - if [ -z "$RUN_DIR" ]; then - echo "ERROR: No run directory found" - exit 1 - fi - echo "Latest run directory: $RUN_DIR" + # Get most recently modified run directory + RUN_DIR=$(ls -t "$HOME/.foc-devnet/run" | head -1) + echo "Most recently modified run directory: $RUN_DIR" - DEVNET_INFO="${RUN_DIR}devnet-info.json" + DEVNET_INFO="$HOME/.foc-devnet/run/${RUN_DIR}/devnet-info.json" if [ ! -f "$DEVNET_INFO" ]; then echo "ERROR: devnet-info.json was not created at $DEVNET_INFO" echo "Contents of run directory:" - ls -la "$RUN_DIR" + ls -la "$HOME/.foc-devnet/run/${RUN_DIR}" exit 1 fi echo "✓ devnet-info.json exists at $DEVNET_INFO" @@ -368,9 +364,8 @@ jobs: - name: "CHECK: {Verify read-devnet-info.js works}" if: steps.start_cluster.outcome == 'success' run: | - # Find the latest run directory - RUN_DIR=$(ls -td "$HOME/.foc-devnet/run"/*/ 2>/dev/null | head -1) - DEVNET_INFO="${RUN_DIR}devnet-info.json" + RUN_DIR=$(ls -t "$HOME/.foc-devnet/run" | head -1) + DEVNET_INFO="$HOME/.foc-devnet/run/${RUN_DIR}/devnet-info.json" cd examples npm install From ff98c002729927772413f2a1026c8ef797dd2a4f Mon Sep 17 00:00:00 2001 From: RedPanda Date: Tue, 27 Jan 2026 20:51:38 +0530 Subject: [PATCH 06/30] stablize: symlinks for /state/latest, simplify CI check --- .github/workflows/ci.yml | 67 +++++++------------------------ src/logger.rs | 31 ++------------ src/run_id/mod.rs | 87 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 101 insertions(+), 84 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1630737..4a791004 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -272,27 +272,16 @@ jobs: # On failure, collect and print Docker container logs for debugging - name: "EXEC: {Collect Docker logs on failure}, independent" run: | - echo "+++++++++++ foc-devnet version run" - cat ~/.foc-devnet/state/latest/version.txt 2>/dev/null || echo "No version file found" - - echo "+++++++++++ Listing runs in foc-devnet..." - ls -1 ~/.foc-devnet/run/ 2>/dev/null || echo "No runs directory found" - - echo "+++++++++++ Get latest run ID" - LATEST_RUN=$(ls -t ~/.foc-devnet/run/ 2>/dev/null | head -1) - if [ -n "$LATEST_RUN" ]; then - echo "Latest run: $LATEST_RUN" - RUN_DIR="$HOME/.foc-devnet/run/$LATEST_RUN" - else - RUN_DIR="$HOME/.foc-devnet/state/latest" - fi + RUN_DIR="$HOME/.foc-devnet/state/latest" + + echo "+++++++++++ foc-devnet version" + cat "$RUN_DIR/version.txt" 2>/dev/null || echo "No version file found" - echo "+++++++++++ Disk space..." + echo "+++++++++++ Disk space" sudo df -h 2>/dev/null || echo "df command failed" - echo "+++++++++++ Latest Run Directory" - ls -lath "$RUN_DIR" 2>/dev/null || echo "No run directory found at $RUN_DIR" - ls -lath "$RUN_DIR/logs" 2>/dev/null || echo "No logs directory found at $RUN_DIR/logs" + echo "+++++++++++ Run Directory Contents" + ls -lath "$RUN_DIR" 2>/dev/null || echo "No run directory found" echo "+++++++++++ Contract Addresses" cat "$RUN_DIR/contract_addresses.json" 2>/dev/null || echo "No contract addresses file found" @@ -300,7 +289,7 @@ jobs: echo "+++++++++++ Step Context" cat "$RUN_DIR/step_context.json" 2>/dev/null || echo "No step context file found" - echo "+++++++++++ FOC metadata" + echo "+++++++++++ FOC Metadata" cat "$RUN_DIR/foc_metadata.json" 2>/dev/null || echo "No foc metadata file found" echo "+++++++++++ Container Logs" @@ -308,13 +297,12 @@ jobs: for logfile in "$RUN_DIR/logs"/*; do if [ -f "$logfile" ]; then echo "" - echo "📰📰📰📰📰📰📰📰📰📰📰 Logs from $(basename "$logfile") 📰📰📰📰📰📰📰📰📰📰📰📰📰" + echo "📰 Logs from $(basename "$logfile") 📰" cat "$logfile" 2>/dev/null || echo "Failed to read $logfile" - echo "📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰📰" fi done else - echo "No container logs directory found at $RUN_DIR/logs" + echo "No container logs directory found" fi # Verify cluster is running correctly @@ -330,43 +318,16 @@ jobs: - name: "CHECK: {Verify devnet-info.json exists}" if: steps.start_cluster.outcome == 'success' run: | - # Get most recently modified run directory - RUN_DIR=$(ls -t "$HOME/.foc-devnet/run" | head -1) - echo "Most recently modified run directory: $RUN_DIR" - - DEVNET_INFO="$HOME/.foc-devnet/run/${RUN_DIR}/devnet-info.json" - if [ ! -f "$DEVNET_INFO" ]; then - echo "ERROR: devnet-info.json was not created at $DEVNET_INFO" - echo "Contents of run directory:" - ls -la "$HOME/.foc-devnet/run/${RUN_DIR}" - exit 1 - fi - echo "✓ devnet-info.json exists at $DEVNET_INFO" - echo "--- Contents ---" + DEVNET_INFO="$HOME/.foc-devnet/state/latest/devnet-info.json" + test -f "$DEVNET_INFO" || exit 1 + echo "✓ devnet-info.json created" cat "$DEVNET_INFO" - echo "" - echo "--- Schema validation ---" - VERSION=$(cat "$DEVNET_INFO" | jq -r '.version') - RUN_ID=$(cat "$DEVNET_INFO" | jq -r '.info.run_id') - echo "Schema version: $VERSION" - echo "Run ID: $RUN_ID" - if [ "$VERSION" != "1" ]; then - echo "ERROR: Unexpected schema version: $VERSION" - exit 1 - fi - if [ -z "$RUN_ID" ] || [ "$RUN_ID" == "null" ]; then - echo "ERROR: run_id is missing or null" - exit 1 - fi - echo "✓ devnet-info.json schema is valid" # Run the JavaScript example to verify it works - name: "CHECK: {Verify read-devnet-info.js works}" if: steps.start_cluster.outcome == 'success' run: | - RUN_DIR=$(ls -t "$HOME/.foc-devnet/run" | head -1) - DEVNET_INFO="$HOME/.foc-devnet/run/${RUN_DIR}/devnet-info.json" - + DEVNET_INFO="$HOME/.foc-devnet/state/latest/devnet-info.json" cd examples npm install node read-devnet-info.js "$DEVNET_INFO" diff --git a/src/logger.rs b/src/logger.rs index 095e99ec..aac845d7 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,7 +1,5 @@ -use crate::paths::{foc_devnet_run_dir, foc_devnet_run_log_file, foc_devnet_state_latest}; +use crate::paths::foc_devnet_run_log_file; use std::fs; -use std::os::unix::fs::symlink; -use std::path::Path; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; /// Initializes the logging system. @@ -10,9 +8,10 @@ use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, Env /// 1. A stdout layer with ANSI colors for terminal output. /// 2. A file layer without ANSI colors for the execution log. /// -/// It also updates the `state/latest` symlink to point to the current run directory. +/// Note: The `state/latest` symlink is managed by the start command's +/// `setup_directories_and_run_id()` function to ensure proper sequencing. pub fn init_logging(run_id: &str) -> Result<(), Box> { - let run_dir = foc_devnet_run_dir(run_id); + let run_dir = crate::paths::foc_devnet_run_dir(run_id); fs::create_dir_all(&run_dir)?; let log_file_path = foc_devnet_run_log_file(run_id); @@ -34,27 +33,5 @@ pub fn init_logging(run_id: &str) -> Result<(), Box> { .with(stdout_layer) .init(); - update_latest_symlink(&run_dir)?; - Ok(()) -} - -fn update_latest_symlink(run_dir: &Path) -> Result<(), Box> { - let latest = foc_devnet_state_latest(); - - // Remove existing symlink or directory if it exists - if latest.exists() || latest.is_symlink() { - if latest.is_symlink() || latest.is_file() { - fs::remove_file(&latest)?; - } else if latest.is_dir() { - fs::remove_dir_all(&latest)?; - } - } - - // Ensure parent directory exists - if let Some(parent) = latest.parent() { - fs::create_dir_all(parent)?; - } - - symlink(run_dir, latest)?; Ok(()) } diff --git a/src/run_id/mod.rs b/src/run_id/mod.rs index 0648ca9a..2b954dbb 100644 --- a/src/run_id/mod.rs +++ b/src/run_id/mod.rs @@ -57,23 +57,50 @@ pub fn generate_run_id() -> String { } /// Create a symlink to the latest run directory. +/// +/// This function is responsible for maintaining `~/.foc-devnet/state/latest`, +/// a symlink that always points to the most recent run directory. +/// +/// It handles: +/// - Creating the parent directory if needed +/// - Removing any existing symlink (including broken ones) +/// - Creating the new symlink +/// +/// # Arguments +/// * `run_id` - The run ID of the current execution +/// +/// # Failures +/// This is a critical operation. If it fails, the state directory will be +/// inconsistent and subsequent runs may have issues. pub fn create_latest_symlink(run_id: &str) -> Result<(), Box> { let latest_link = crate::paths::foc_devnet_state_latest(); let run_dir = crate::paths::foc_devnet_run_dir(run_id); - // Remove existing symlink if it exists - if latest_link.exists() || latest_link.is_symlink() { + // Ensure parent directory exists (state/) before trying to create symlink + if let Some(parent) = latest_link.parent() { + std::fs::create_dir_all(parent)?; + } + + // Remove existing symlink if it exists (including broken symlinks) + // We need to handle broken symlinks carefully: + // - exists() returns false for broken symlinks (target doesn't exist) + // - is_symlink() returns true even if target is broken + // So we check is_symlink() first, which is true for both valid and broken symlinks + if latest_link.is_symlink() { #[cfg(unix)] std::fs::remove_file(&latest_link)?; #[cfg(windows)] + std::fs::remove_file(&latest_link)?; + } else if latest_link.exists() { + // It's a real directory or file (shouldn't happen, but handle it) if latest_link.is_dir() { - std::fs::remove_dir(&latest_link)?; + std::fs::remove_dir_all(&latest_link)?; } else { std::fs::remove_file(&latest_link)?; } } - // Create new symlink + // Create new symlink pointing to the run directory #[cfg(unix)] std::os::unix::fs::symlink(&run_dir, &latest_link)?; #[cfg(windows)] @@ -110,4 +137,56 @@ mod tests { // At least the random name part should differ assert_ne!(id1, id2, "Run IDs should be different"); } + + #[test] + fn test_create_latest_symlink_handles_broken_symlinks() { + // This test verifies that create_latest_symlink can remove broken symlinks + use std::os::unix::fs::symlink; + use tempfile::TempDir; + + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let run_id = generate_run_id(); + + // Create mock run directories and state path + let runs_dir = temp_dir.path().join("runs"); + let state_dir = temp_dir.path().join("state"); + let latest_link = state_dir.join("latest"); + + std::fs::create_dir_all(&runs_dir).expect("Failed to create runs dir"); + std::fs::create_dir_all(&state_dir).expect("Failed to create state dir"); + + // Create a run directory + let run_dir = runs_dir.join(&run_id); + std::fs::create_dir_all(&run_dir).expect("Failed to create run dir"); + + // First, create the symlink + symlink(&run_dir, &latest_link).expect("Failed to create initial symlink"); + assert!(latest_link.is_symlink(), "Initial symlink should exist"); + + // Now delete the target to create a broken symlink + std::fs::remove_dir_all(&run_dir).expect("Failed to remove run dir"); + assert!( + latest_link.is_symlink(), + "Broken symlink should still be detected as symlink" + ); + assert!( + !latest_link.exists(), + "Broken symlink target should not exist" + ); + + // Recreate the run directory + std::fs::create_dir_all(&run_dir).expect("Failed to recreate run dir"); + + // Now test that we can remove the broken symlink and create a new one + if latest_link.is_symlink() { + std::fs::remove_file(&latest_link).expect("Failed to remove broken symlink"); + } + assert!(!latest_link.exists(), "Symlink should be removed"); + + symlink(&run_dir, &latest_link).expect("Failed to create new symlink"); + assert!( + latest_link.is_symlink(), + "New symlink should exist and point to correct target" + ); + } } From fd96904e1e2e7c4d567d5d1f5fc11bb8641de80c Mon Sep 17 00:00:00 2001 From: RedPanda Date: Wed, 28 Jan 2026 09:09:23 +0530 Subject: [PATCH 07/30] add: nodejs 20 installation --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a791004..d989f976 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -323,6 +323,13 @@ jobs: echo "✓ devnet-info.json created" cat "$DEVNET_INFO" + # Setup Node.js for JavaScript examples + - name: "EXEC: {Setup Node.js}, independent" + if: steps.start_cluster.outcome == 'success' + uses: actions/setup-node@v4 + with: + node-version: '20' + # Run the JavaScript example to verify it works - name: "CHECK: {Verify read-devnet-info.js works}" if: steps.start_cluster.outcome == 'success' From 6bba3f05df6b3148f81929070e8f58e63c3da35c Mon Sep 17 00:00:00 2001 From: RedPanda Date: Wed, 28 Jan 2026 09:33:28 +0530 Subject: [PATCH 08/30] Update .github/workflows/ci.yml, use nodejs LTS Co-authored-by: Rod Vagg --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d989f976..c8204e48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -326,9 +326,9 @@ jobs: # Setup Node.js for JavaScript examples - name: "EXEC: {Setup Node.js}, independent" if: steps.start_cluster.outcome == 'success' - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '20' + node-version: lts/* # Run the JavaScript example to verify it works - name: "CHECK: {Verify read-devnet-info.js works}" From 14c6f6d90040f82cee32e964484c045da4cec1eb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:45:31 +0530 Subject: [PATCH 09/30] Extract magic number for user account count to constant (#46) * Initial plan * Extract magic number 3 to USER_ACCOUNT_COUNT constant Co-authored-by: redpanda-f <181817029+redpanda-f@users.noreply.github.com> * Improve USER_ACCOUNT_COUNT constant documentation Co-authored-by: redpanda-f <181817029+redpanda-f@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: redpanda-f <181817029+redpanda-f@users.noreply.github.com> --- src/commands/start/usdfc_funding/usdfc_funding_step.rs | 7 ++++--- src/constants.rs | 3 +++ src/external_api/export.rs | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/commands/start/usdfc_funding/usdfc_funding_step.rs b/src/commands/start/usdfc_funding/usdfc_funding_step.rs index d287b9a8..299e0f1c 100644 --- a/src/commands/start/usdfc_funding/usdfc_funding_step.rs +++ b/src/commands/start/usdfc_funding/usdfc_funding_step.rs @@ -8,6 +8,7 @@ use super::funding_operations::{self, check_mock_usdfc_balance, transfer_mock_us use super::key_operations::get_user_private_key; use crate::commands::start::step::{SetupContext, Step}; use crate::commands::start::usdfc_funding::key_operations::get_user_eth_address; +use crate::constants::USER_ACCOUNT_COUNT; use crate::docker::containers::lotus_container_name; use crate::docker::core::container_is_running; use std::error::Error; @@ -41,7 +42,7 @@ impl USDFCFundingStep { let mut accounts_to_check = Vec::new(); // Add user accounts (base-1 numbering) - for user_num in 1..=3 { + for user_num in 1..=USER_ACCOUNT_COUNT { let account_name = format!("USER_{}", user_num); accounts_to_check.push((account_name, 100_000u64)); } @@ -250,7 +251,7 @@ impl Step for USDFCFundingStep { let mut token_transfers = Vec::new(); // Add user accounts (base-1 numbering) - for user_num in 1..=3 { + for user_num in 1..=USER_ACCOUNT_COUNT { let account_name = format!("USER_{}", user_num); let eth_address = get_user_eth_address(&account_name)?; let amount_tokens = 100_000u64; @@ -291,7 +292,7 @@ impl Step for USDFCFundingStep { // Build list of accounts to verify let mut accounts_to_verify = Vec::new(); - for user_num in 1..=3 { + for user_num in 1..=USER_ACCOUNT_COUNT { let account_name = format!("USER_{}", user_num); accounts_to_verify.push((account_name, 100_000u64)); } diff --git a/src/constants.rs b/src/constants.rs index 64532323..b6f356ab 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -41,6 +41,9 @@ pub const PORT_CHECK_TIMEOUT_MS: u64 = 5000; /// PDP Service Provider configuration pub const MAX_PDP_SP_COUNT: usize = 5; +/// Number of user test accounts (USER_1, USER_2, USER_3) +pub const USER_ACCOUNT_COUNT: usize = 3; + /// Service configuration pub const SERVICE_NAME: &str = "FOC DevNet Warm Storage"; pub const SERVICE_DESCRIPTION: &str = "Warm storage service for FOC local development network"; diff --git a/src/external_api/export.rs b/src/external_api/export.rs index 720f1182..3aa2d2d9 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -8,6 +8,7 @@ use std::path::Path; use chrono::Utc; use crate::commands::start::step::SetupContext; +use crate::constants::USER_ACCOUNT_COUNT; use crate::crypto::derive_ethereum_key; use crate::crypto::mnemonic::load_mnemonic; use crate::external_api::{ @@ -55,7 +56,7 @@ fn build_users(ctx: &SetupContext) -> Result, Box Date: Thu, 29 Jan 2026 10:28:36 +0530 Subject: [PATCH 10/30] item 1: remove balance fields from export --- .gitignore | 3 ++- src/external_api/devnet_info.rs | 4 ---- src/external_api/export.rs | 21 --------------------- 3 files changed, 2 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index c1654137..8a22f1cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target/ contracts/MockUSDFC/lib/ contracts/MockUSDFC/broadcast/ -artifacts/ \ No newline at end of file +artifacts/ +.vscode/ \ No newline at end of file diff --git a/src/external_api/devnet_info.rs b/src/external_api/devnet_info.rs index 7e01c540..71c25de6 100644 --- a/src/external_api/devnet_info.rs +++ b/src/external_api/devnet_info.rs @@ -48,10 +48,6 @@ pub struct UserInfo { pub evm_addr: String, /// Native Filecoin address (t410f...) pub native_addr: String, - /// Balance in tFIL - pub native_balance_tfil: String, - /// Balance in MockUSDFC tokens - pub mockusdfc_balance: String, /// Private key in hex format (without 0x prefix) pub private_key_hex: String, } diff --git a/src/external_api/export.rs b/src/external_api/export.rs index 3aa2d2d9..29328048 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -82,35 +82,14 @@ fn build_single_user( .get(&format!("{}_address", name.to_lowercase())) .unwrap_or_else(|| derived.native_address.clone()); - // Format USDFC balance: 100,000 tokens with 18 decimals = 100000000000000000000000 wei - // But display in human-readable form with decimals - let mockusdfc_wei = "100000000000000000000000"; // 100,000 USDFC - let mockusdfc_formatted = format_token_balance(mockusdfc_wei); - Ok(UserInfo { name: name.to_string(), evm_addr, native_addr, - native_balance_tfil: "1000".to_string(), // Default funding amount - mockusdfc_balance: mockusdfc_formatted, private_key_hex: format!("0x{}", derived.private_key), }) } -/// Format token balance from wei to human-readable form with 18 decimals -fn format_token_balance(wei: &str) -> String { - if wei.len() <= 18 { - // Less than 1 token - let padded = format!("{:0>18}", wei); - format!("0.{}", padded) - } else { - let split_point = wei.len() - 18; - let whole = &wei[..split_point]; - let fraction = &wei[split_point..]; - format!("{}.{}", whole, fraction) - } -} - /// Build contracts info from context. fn build_contracts(ctx: &SetupContext) -> ContractsInfo { ContractsInfo { From fcd2727a1422054c11ecb02f3ae781a3def8d2d6 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 10:31:13 +0530 Subject: [PATCH 11/30] item 2: paths as source of truth --- src/commands/start/mod.rs | 2 +- src/external_api/export.rs | 5 +++-- src/external_api/mod.rs | 3 --- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/commands/start/mod.rs b/src/commands/start/mod.rs index 9da5a9f5..0d8d6194 100644 --- a/src/commands/start/mod.rs +++ b/src/commands/start/mod.rs @@ -447,7 +447,7 @@ pub fn start_cluster( } else { info!( "✓ DevNet info exported to: {}", - context.run_dir().join("devnet-info.json").display() + crate::paths::devnet_info_file(&context.run_id().to_string()).display() ); } info!("Cluster started successfully!"); diff --git a/src/external_api/export.rs b/src/external_api/export.rs index 29328048..d64083e6 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -13,8 +13,9 @@ use crate::crypto::derive_ethereum_key; use crate::crypto::mnemonic::load_mnemonic; use crate::external_api::{ ContractsInfo, CurioInfo, DevnetInfoV1, LotusInfo, LotusMinerInfo, UserInfo, - VersionedDevnetInfo, YugabyteInfo, DEVNET_INFO_FILENAME, DEVNET_INFO_SCHEMA_VERSION, + VersionedDevnetInfo, YugabyteInfo, DEVNET_INFO_SCHEMA_VERSION, }; +use crate::paths; /// Export DevNet information to a JSON file. /// @@ -27,7 +28,7 @@ pub fn export_devnet_info(context: &SetupContext) -> Result<(), Box Date: Thu, 29 Jan 2026 10:32:45 +0530 Subject: [PATCH 12/30] item 3: error handling with Result --- src/external_api/export.rs | 68 +++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/src/external_api/export.rs b/src/external_api/export.rs index d64083e6..7c75e96e 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -44,9 +44,9 @@ fn build_devnet_info(ctx: &SetupContext) -> Result ContractsInfo { - ContractsInfo { - multicall3_addr: ctx.get("multicall3_address").unwrap_or_default(), - mockusdfc_addr: ctx.get("mockusdfc_contract_address").unwrap_or_default(), +fn build_contracts(ctx: &SetupContext) -> Result> { + Ok(ContractsInfo { + multicall3_addr: ctx + .get("multicall3_address") + .ok_or("Missing multicall3_address in context")?, + mockusdfc_addr: ctx + .get("mockusdfc_contract_address") + .ok_or("Missing mockusdfc_contract_address in context")?, fwss_service_proxy_addr: ctx .get("foc_contract_filecoin_warm_storage_service_proxy") - .unwrap_or_default(), + .ok_or("Missing foc_contract_filecoin_warm_storage_service_proxy in context")?, fwss_state_view_addr: ctx .get("foc_contract_filecoin_warm_storage_service_state_view") - .unwrap_or_default(), + .ok_or("Missing foc_contract_filecoin_warm_storage_service_state_view in context")?, fwss_impl_addr: ctx .get("foc_contract_filecoin_warm_storage_service_implementation") - .unwrap_or_default(), + .ok_or("Missing foc_contract_filecoin_warm_storage_service_implementation in context")?, pdp_verifier_proxy_addr: ctx .get("foc_contract_p_d_p_verifier_proxy") - .unwrap_or_default(), + .ok_or("Missing foc_contract_p_d_p_verifier_proxy in context")?, pdp_verifier_impl_addr: ctx .get("foc_contract_p_d_p_verifier_implementation") - .unwrap_or_default(), + .ok_or("Missing foc_contract_p_d_p_verifier_implementation in context")?, service_provider_registry_proxy_addr: ctx .get("foc_contract_service_provider_registry_proxy") - .unwrap_or_default(), + .ok_or("Missing foc_contract_service_provider_registry_proxy in context")?, service_provider_registry_impl_addr: ctx .get("foc_contract_service_provider_registry_implementation") - .unwrap_or_default(), + .ok_or("Missing foc_contract_service_provider_registry_implementation in context")?, filecoin_pay_v1_addr: ctx .get("foc_contract_filecoin_pay_v1_contract") - .unwrap_or_default(), - endorsements_addr: ctx.get("foc_contract_endorsements").unwrap_or_default(), - } + .ok_or("Missing foc_contract_filecoin_pay_v1_contract in context")?, + endorsements_addr: ctx + .get("foc_contract_endorsements") + .ok_or("Missing foc_contract_endorsements in context")?, + }) } /// Build Lotus node info from context. -fn build_lotus_info(ctx: &SetupContext) -> LotusInfo { +fn build_lotus_info(ctx: &SetupContext) -> Result> { let api_port = ctx .get("lotus_api_port") .unwrap_or_else(|| "1234".to_string()); - LotusInfo { + Ok(LotusInfo { host_rpc_url: format!("http://localhost:{}/rpc/v1", api_port), - container_id: ctx.get("lotus_container_id").unwrap_or_default(), - container_name: ctx.get("lotus_container_name").unwrap_or_default(), - } + container_id: ctx + .get("lotus_container_id") + .ok_or("Missing lotus_container_id in context")?, + container_name: ctx + .get("lotus_container_name") + .ok_or("Missing lotus_container_name in context")?, + }) } /// Build Lotus miner info from context. -fn build_lotus_miner_info(ctx: &SetupContext) -> LotusMinerInfo { +fn build_lotus_miner_info(ctx: &SetupContext) -> Result> { let api_port: u16 = ctx .get("lotus_miner_api_port") .and_then(|p| p.parse().ok()) .unwrap_or(2345); - LotusMinerInfo { - container_id: ctx.get("lotus_miner_container_id").unwrap_or_default(), - container_name: ctx.get("lotus_miner_container_name").unwrap_or_default(), + Ok(LotusMinerInfo { + container_id: ctx + .get("lotus_miner_container_id") + .ok_or("Missing lotus_miner_container_id in context")?, + container_name: ctx + .get("lotus_miner_container_name") + .ok_or("Missing lotus_miner_container_name in context")?, api_port, - } + }) } /// Build Curio providers info from context. From 9e318383e1d86b9ee5f5725371a56d2762698c97 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 10:34:35 +0530 Subject: [PATCH 13/30] item 5: improve symlink test --- src/run_id/mod.rs | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/run_id/mod.rs b/src/run_id/mod.rs index 2b954dbb..6a65fa39 100644 --- a/src/run_id/mod.rs +++ b/src/run_id/mod.rs @@ -140,31 +140,37 @@ mod tests { #[test] fn test_create_latest_symlink_handles_broken_symlinks() { - // This test verifies that create_latest_symlink can remove broken symlinks + // This test verifies that create_latest_symlink properly handles + // removing broken symlinks and creating new ones use std::os::unix::fs::symlink; use tempfile::TempDir; let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let run_id = generate_run_id(); + let _run_id = generate_run_id(); - // Create mock run directories and state path - let runs_dir = temp_dir.path().join("runs"); + // Mock the paths by using temp directory structure + let runs_dir = temp_dir.path().join("run"); let state_dir = temp_dir.path().join("state"); let latest_link = state_dir.join("latest"); std::fs::create_dir_all(&runs_dir).expect("Failed to create runs dir"); std::fs::create_dir_all(&state_dir).expect("Failed to create state dir"); - // Create a run directory - let run_dir = runs_dir.join(&run_id); - std::fs::create_dir_all(&run_dir).expect("Failed to create run dir"); + // Create first run directory + let run_dir1 = runs_dir.join("run1"); + std::fs::create_dir_all(&run_dir1).expect("Failed to create run1 dir"); - // First, create the symlink - symlink(&run_dir, &latest_link).expect("Failed to create initial symlink"); + // Create initial symlink + symlink(&run_dir1, &latest_link).expect("Failed to create initial symlink"); assert!(latest_link.is_symlink(), "Initial symlink should exist"); + assert_eq!( + std::fs::read_link(&latest_link).unwrap(), + run_dir1, + "Symlink should point to run1" + ); - // Now delete the target to create a broken symlink - std::fs::remove_dir_all(&run_dir).expect("Failed to remove run dir"); + // Delete the target to create a broken symlink + std::fs::remove_dir_all(&run_dir1).expect("Failed to remove run1 dir"); assert!( latest_link.is_symlink(), "Broken symlink should still be detected as symlink" @@ -174,19 +180,23 @@ mod tests { "Broken symlink target should not exist" ); - // Recreate the run directory - std::fs::create_dir_all(&run_dir).expect("Failed to recreate run dir"); + // Create a new run directory + let run_dir2 = runs_dir.join("run2"); + std::fs::create_dir_all(&run_dir2).expect("Failed to create run2 dir"); - // Now test that we can remove the broken symlink and create a new one + // Remove the broken symlink and create new one if latest_link.is_symlink() { std::fs::remove_file(&latest_link).expect("Failed to remove broken symlink"); } assert!(!latest_link.exists(), "Symlink should be removed"); - symlink(&run_dir, &latest_link).expect("Failed to create new symlink"); - assert!( - latest_link.is_symlink(), - "New symlink should exist and point to correct target" + symlink(&run_dir2, &latest_link).expect("Failed to create new symlink"); + assert!(latest_link.is_symlink(), "New symlink should exist"); + assert_eq!( + std::fs::read_link(&latest_link).unwrap(), + run_dir2, + "Symlink should point to run2" ); + assert!(latest_link.exists(), "New symlink target should exist"); } } From 593e4e0b74a62db92f3966664924753416b17e83 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 10:40:28 +0530 Subject: [PATCH 14/30] item 6: add zod schema validation --- .github/workflows/ci.yml | 16 +++++-- examples/devnet-schema.js | 90 ++++++++++++++++++++++++++++++++++++ examples/package.json | 5 +- examples/read-devnet-info.js | 15 ++---- examples/validate-schema.js | 28 +++++++++++ 5 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 examples/devnet-schema.js create mode 100644 examples/validate-schema.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8204e48..7db9146b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -321,17 +321,25 @@ jobs: DEVNET_INFO="$HOME/.foc-devnet/state/latest/devnet-info.json" test -f "$DEVNET_INFO" || exit 1 echo "✓ devnet-info.json created" - cat "$DEVNET_INFO" + jq '.version' "$DEVNET_INFO" # Setup Node.js for JavaScript examples - name: "EXEC: {Setup Node.js}, independent" if: steps.start_cluster.outcome == 'success' - uses: actions/setup-node@v6 + uses: actions/setup-node@v4 with: - node-version: lts/* + node-version: '20' + + # Validate schema using zod + - name: "CHECK: {Validate devnet-info.json schema}" + if: steps.start_cluster.outcome == 'success' + run: | + DEVNET_INFO="$HOME/.foc-devnet/state/latest/devnet-info.json" + cd examples + npm install + node validate-schema.js "$DEVNET_INFO" # Run the JavaScript example to verify it works - - name: "CHECK: {Verify read-devnet-info.js works}" if: steps.start_cluster.outcome == 'success' run: | DEVNET_INFO="$HOME/.foc-devnet/state/latest/devnet-info.json" diff --git a/examples/devnet-schema.js b/examples/devnet-schema.js new file mode 100644 index 00000000..d15aa2ed --- /dev/null +++ b/examples/devnet-schema.js @@ -0,0 +1,90 @@ +/** + * Zod schema for validating DevNet info exports. + * + * This schema ensures strict type checking and validates that all required + * fields are present with correct types. It's used by CI and examples to + * validate the exported devnet-info.json file. + */ + +import { z } from "zod"; + +const YugabyteInfo = z.object({ + web_ui_url: z.string().url(), + master_rpc_port: z.number().int().positive(), + ysql_port: z.number().int().positive(), +}); + +const CurioInfo = z.object({ + provider_id: z.number().int().positive(), + eth_addr: z.string().startsWith("0x"), + native_addr: z.string().min(1), + pdp_service_url: z.string().url(), + container_id: z.string().min(1), + container_name: z.string().min(1), + yugabyte: YugabyteInfo, +}); + +const ContractsInfo = z.object({ + multicall3_addr: z.string().startsWith("0x"), + mockusdfc_addr: z.string().startsWith("0x"), + fwss_service_proxy_addr: z.string().startsWith("0x"), + fwss_state_view_addr: z.string().startsWith("0x"), + fwss_impl_addr: z.string().startsWith("0x"), + pdp_verifier_proxy_addr: z.string().startsWith("0x"), + pdp_verifier_impl_addr: z.string().startsWith("0x"), + service_provider_registry_proxy_addr: z.string().startsWith("0x"), + service_provider_registry_impl_addr: z.string().startsWith("0x"), + filecoin_pay_v1_addr: z.string().startsWith("0x"), + endorsements_addr: z.string().startsWith("0x"), +}); + +const UserInfo = z.object({ + name: z.string().regex(/^USER_\d+$/), + evm_addr: z.string().startsWith("0x"), + native_addr: z.string().min(1), + private_key_hex: z.string().startsWith("0x"), +}); + +const LotusInfo = z.object({ + host_rpc_url: z.string().url(), + container_id: z.string().min(1), + container_name: z.string().min(1), +}); + +const LotusMinerInfo = z.object({ + container_id: z.string().min(1), + container_name: z.string().min(1), + api_port: z.number().int().positive(), +}); + +const DevnetInfoV1 = z.object({ + run_id: z.string().min(1), + start_time: z.string().datetime(), + startup_duration: z.string().min(1), + users: z.array(UserInfo).min(1), + contracts: ContractsInfo, + lotus: LotusInfo, + lotus_miner: LotusMinerInfo, + pdp_sps: z.array(CurioInfo).min(1), +}); + +export const VersionedDevnetInfo = z.object({ + version: z.literal(1), + info: DevnetInfoV1, +}); + +/** + * Validate DevNet info against schema. + * @param {object} data - The parsed JSON data to validate + * @returns {object} The validated data if successful + * @throws {Error} If validation fails + */ +export function validateDevnetInfo(data) { + try { + return VersionedDevnetInfo.parse(data); + } catch (error) { + throw new Error( + `DevNet info schema validation failed: ${error.message}` + ); + } +} diff --git a/examples/package.json b/examples/package.json index 0704f46f..7cf7b21f 100644 --- a/examples/package.json +++ b/examples/package.json @@ -8,10 +8,11 @@ "check-balances": "node check-balances.js" }, "dependencies": { - "ethers": "^6.0.0" + "ethers": "^6.0.0", + "zod": "^3.22.0" }, "devDependencies": {}, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/examples/read-devnet-info.js b/examples/read-devnet-info.js index b591a44d..c6636ea5 100644 --- a/examples/read-devnet-info.js +++ b/examples/read-devnet-info.js @@ -13,13 +13,12 @@ import { readFileSync, existsSync } from "fs"; import { homedir } from "os"; import { join } from "path"; - -const SCHEMA_VERSION = 1; +import { validateDevnetInfo } from "./devnet-schema.js"; /** * Load and validate the devnet-info.json file. * @param {string} filePath - Path to the devnet-info.json file - * @returns {object} The parsed DevNet info + * @returns {object} The parsed and validated DevNet info */ function loadDevnetInfo(filePath) { if (!existsSync(filePath)) { @@ -29,14 +28,8 @@ function loadDevnetInfo(filePath) { const content = readFileSync(filePath, "utf8"); const data = JSON.parse(content); - // Validate schema version - if (data.version !== SCHEMA_VERSION) { - console.warn( - `Warning: Expected schema version ${SCHEMA_VERSION}, got ${data.version}` - ); - } - - return data; + // Validate against schema - this will throw if invalid + return validateDevnetInfo(data); } /** diff --git a/examples/validate-schema.js b/examples/validate-schema.js new file mode 100644 index 00000000..6bd2ef40 --- /dev/null +++ b/examples/validate-schema.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node +/** + * Schema validation script for DevNet info export. + * Used by CI to validate the exported devnet-info.json file. + */ + +import { readFileSync } from "fs"; +import { validateDevnetInfo } from "./devnet-schema.js"; + +const devnetInfoPath = process.argv[2]; + +if (!devnetInfoPath) { + console.error("Usage: node validate-schema.js "); + process.exit(1); +} + +try { + const content = readFileSync(devnetInfoPath, "utf8"); + const data = JSON.parse(content); + + validateDevnetInfo(data); + console.log("✓ Schema validation passed"); + process.exit(0); +} catch (error) { + console.error("✗ Schema validation failed:"); + console.error(error.message); + process.exit(1); +} From df0a4345d96b9230e73d82335906be031d0d5b3e Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 10:41:20 +0530 Subject: [PATCH 15/30] item 7: remove dead code --- .github/workflows/ci.yml | 7 ------- examples/read-devnet-info.js | 20 -------------------- 2 files changed, 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7db9146b..7b964123 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -338,13 +338,6 @@ jobs: cd examples npm install node validate-schema.js "$DEVNET_INFO" - - # Run the JavaScript example to verify it works - if: steps.start_cluster.outcome == 'success' - run: | - DEVNET_INFO="$HOME/.foc-devnet/state/latest/devnet-info.json" - cd examples - npm install node read-devnet-info.js "$DEVNET_INFO" echo "✓ read-devnet-info.js executed successfully" diff --git a/examples/read-devnet-info.js b/examples/read-devnet-info.js index c6636ea5..8edda744 100644 --- a/examples/read-devnet-info.js +++ b/examples/read-devnet-info.js @@ -111,26 +111,6 @@ function printCurioProviders(providers) { } } -/** - * Format a token balance from wei to human-readable form. - * @param {string} balance - Balance in wei (as string) - * @param {number} decimals - Token decimals - * @returns {string} Formatted balance - */ -function formatTokenBalance(balance, decimals) { - try { - const balanceBigInt = BigInt(balance); - const divisor = BigInt(10 ** decimals); - const whole = balanceBigInt / divisor; - const fraction = balanceBigInt % divisor; - const fractionStr = fraction.toString().padStart(decimals, "0"); - return `${whole}.${fractionStr.substring(0, 4)}`; - } catch (e) { - // If balance is already formatted, return as-is - return balance; - } -} - // Main execution function main() { // Determine file path From 8b3797f2523138acdad02e996b7bd5f06d206fd4 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 10:42:35 +0530 Subject: [PATCH 16/30] item 9: rename curio_providers to pdp --- examples/README.md | 2 +- src/external_api/export.rs | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/examples/README.md b/examples/README.md index c4250392..162d5451 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,7 +48,7 @@ The `devnet-info.json` file contains: "contracts": {...}, "lotus": {...}, "lotus_miner": {...}, - "curio_providers": [...] + "pdp_sps": [...] } } ``` diff --git a/src/external_api/export.rs b/src/external_api/export.rs index 7c75e96e..c04c3471 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -47,7 +47,7 @@ fn build_devnet_info(ctx: &SetupContext) -> Result Result Result Result> { +fn build_lotus_miner_info( + ctx: &SetupContext, +) -> Result> { let api_port: u16 = ctx .get("lotus_miner_api_port") .and_then(|p| p.parse().ok()) @@ -164,20 +168,20 @@ fn build_lotus_miner_info(ctx: &SetupContext) -> Result Vec { +/// Build PDP service providers info from context. +fn build_pdp_service_providers(ctx: &SetupContext) -> Vec { let active_count: usize = ctx .get("active_pdp_sp_count") .and_then(|s| s.parse().ok()) .unwrap_or(1); (1..=active_count) - .filter_map(|id| build_single_curio_provider(ctx, id as u32)) + .filter_map(|id| build_single_pdp_service_provider(ctx, id as u32)) .collect() } -/// Build a single Curio provider's info. -fn build_single_curio_provider(ctx: &SetupContext, provider_id: u32) -> Option { +/// Build a single PDP service provider's info. +fn build_single_pdp_service_provider(ctx: &SetupContext, provider_id: u32) -> Option { let eth_addr = ctx.get(&format!("pdp_sp_{}_eth_address", provider_id))?; let native_addr = ctx .get(&format!("pdp_sp_{}_address", provider_id)) From 9baab25c0f260062714590f766689b1a6a86e1fa Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 10:46:52 +0530 Subject: [PATCH 17/30] fix: remove unnecessary to_string calls --- src/commands/start/mod.rs | 2 +- src/external_api/export.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/start/mod.rs b/src/commands/start/mod.rs index 0d8d6194..99205834 100644 --- a/src/commands/start/mod.rs +++ b/src/commands/start/mod.rs @@ -447,7 +447,7 @@ pub fn start_cluster( } else { info!( "✓ DevNet info exported to: {}", - crate::paths::devnet_info_file(&context.run_id().to_string()).display() + crate::paths::devnet_info_file(context.run_id()).display() ); } info!("Cluster started successfully!"); diff --git a/src/external_api/export.rs b/src/external_api/export.rs index c04c3471..cf1c16fb 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -28,7 +28,7 @@ pub fn export_devnet_info(context: &SetupContext) -> Result<(), Box Date: Thu, 29 Jan 2026 12:33:04 +0530 Subject: [PATCH 18/30] fixes: schema validation --- examples/.eslintrc.json | 20 ++++++++++ examples/devnet-schema.js | 3 +- examples/package.json | 8 +++- src/commands/start/curio/daemon.rs | 39 ++++++++++++++----- src/commands/start/curio/pre_execute.rs | 8 ++-- src/commands/start/curio/verification.rs | 6 +-- .../pdp_service_provider_step.rs | 10 ++++- src/external_api/devnet_info.rs | 2 + src/external_api/export.rs | 12 ++++-- 9 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 examples/.eslintrc.json diff --git a/examples/.eslintrc.json b/examples/.eslintrc.json new file mode 100644 index 00000000..ff2986c3 --- /dev/null +++ b/examples/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "env": { + "node": true, + "es2022": true + }, + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "no-console": "off", + "no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_" + } + ] + } +} diff --git a/examples/devnet-schema.js b/examples/devnet-schema.js index d15aa2ed..93844a6f 100644 --- a/examples/devnet-schema.js +++ b/examples/devnet-schema.js @@ -21,6 +21,7 @@ const CurioInfo = z.object({ pdp_service_url: z.string().url(), container_id: z.string().min(1), container_name: z.string().min(1), + is_approved: z.boolean(), yugabyte: YugabyteInfo, }); @@ -59,7 +60,7 @@ const LotusMinerInfo = z.object({ const DevnetInfoV1 = z.object({ run_id: z.string().min(1), - start_time: z.string().datetime(), + start_time: z.string(), startup_duration: z.string().min(1), users: z.array(UserInfo).min(1), contracts: ContractsInfo, diff --git a/examples/package.json b/examples/package.json index 7cf7b21f..6654d52d 100644 --- a/examples/package.json +++ b/examples/package.json @@ -5,13 +5,17 @@ "type": "module", "scripts": { "read-info": "node read-devnet-info.js", - "check-balances": "node check-balances.js" + "check-balances": "node check-balances.js", + "lint": "eslint *.js", + "lint:fix": "eslint *.js --fix" }, "dependencies": { "ethers": "^6.0.0", "zod": "^3.22.0" }, - "devDependencies": {}, + "devDependencies": { + "eslint": "^8.56.0" + }, "engines": { "node": ">=20.0.0" } diff --git a/src/commands/start/curio/daemon.rs b/src/commands/start/curio/daemon.rs index 18ef72ab..7fd72260 100644 --- a/src/commands/start/curio/daemon.rs +++ b/src/commands/start/curio/daemon.rs @@ -25,8 +25,7 @@ use tracing::info; /// 2. Build Docker run command with proper volumes and env vars /// 3. Start container with sleep infinity /// 4. Run curio daemon in background -/// 5. Wait for API to be ready -/// 6. Store allocated ports in context for later use +/// 5. Store container ID and name in context for later use pub fn start_curio_daemon( context: &SetupContext, _step: &CurioStep, @@ -46,9 +45,19 @@ pub fn start_curio_daemon( // Create necessary directories create_curio_directories(context, sp_index)?; - // Step 2: Create and start container + // Step 2: Create and start container, capturing container ID let docker_args = build_docker_create_args(context, sp_index, &container_name)?; - start_curio_container(context, &container_name, docker_args)?; + let container_id = start_curio_container(context, &container_name, docker_args)?; + + // Store container info in context for export + context.set( + format!("pdp_sp_{}_container_id", sp_index), + container_id, + ); + context.set( + format!("pdp_sp_{}_container_name", sp_index), + container_name, + ); Ok(()) } @@ -79,11 +88,12 @@ fn create_curio_directories(context: &SetupContext, sp_index: usize) -> Result<( /// 1. Container is created but not started /// 2. Networks are connected while container is stopped /// 3. Container is started with Curio as PID 1 (logs work properly) +/// Returns the container ID from docker create stdout fn start_curio_container( context: &SetupContext, container_name: &str, mut docker_args: Vec, -) -> Result<(), Box> { +) -> Result> { info!("Creating container {}...", container_name); // Add image and command - Curio as main process @@ -106,6 +116,15 @@ fn start_curio_container( .into()); } + // Extract container ID from stdout + let container_id = String::from_utf8_lossy(&output.stdout) + .trim() + .to_string(); + + if container_id.is_empty() { + return Err("Docker create did not return container ID".into()); + } + // Connect to filecoin network before starting let lotus_network = lotus_network_name(context.run_id()); let network_args = vec![ @@ -132,7 +151,7 @@ fn start_curio_container( info!("Container created and started"); - Ok(()) + Ok(container_id) } /// Build docker create arguments for Curio @@ -157,19 +176,19 @@ fn build_docker_create_args( // Port mappings - get dynamically allocated ports from context let api_port: u16 = context - .get(&format!("curio_sp_{}_api_port", sp_index)) + .get(&format!("pdp_sp_{}_api_port", sp_index)) .ok_or("Curio API port not found in context")? .parse()?; let api_port_alt: u16 = context - .get(&format!("curio_sp_{}_api_port_alt", sp_index)) + .get(&format!("pdp_sp_{}_api_port_alt", sp_index)) .ok_or("Curio API alt port not found in context")? .parse()?; let gui_port: u16 = context - .get(&format!("curio_sp_{}_gui_port", sp_index)) + .get(&format!("pdp_sp_{}_gui_port", sp_index)) .ok_or("Curio GUI port not found in context")? .parse()?; let pdp_port: u16 = context - .get(&format!("curio_sp_{}_pdp_port", sp_index)) + .get(&format!("pdp_sp_{}_pdp_port", sp_index)) .ok_or("Curio PDP port not found in context")? .parse()?; diff --git a/src/commands/start/curio/pre_execute.rs b/src/commands/start/curio/pre_execute.rs index 65d4bdcf..544b3ae5 100644 --- a/src/commands/start/curio/pre_execute.rs +++ b/src/commands/start/curio/pre_execute.rs @@ -48,19 +48,19 @@ pub fn verify_prerequisites(context: &SetupContext, sp_count: usize) -> Result<( let pdp_port = context.allocate_port()?; context.set( - format!("curio_sp_{}_api_port", sp_index), + format!("pdp_sp_{}_api_port", sp_index), api_port.to_string(), ); context.set( - format!("curio_sp_{}_api_port_alt", sp_index), + format!("pdp_sp_{}_api_port_alt", sp_index), api_port_alt.to_string(), ); context.set( - format!("curio_sp_{}_gui_port", sp_index), + format!("pdp_sp_{}_gui_port", sp_index), gui_port.to_string(), ); context.set( - format!("curio_sp_{}_pdp_port", sp_index), + format!("pdp_sp_{}_pdp_port", sp_index), pdp_port.to_string(), ); diff --git a/src/commands/start/curio/verification.rs b/src/commands/start/curio/verification.rs index 9e18a778..e0fb4d0a 100644 --- a/src/commands/start/curio/verification.rs +++ b/src/commands/start/curio/verification.rs @@ -43,7 +43,7 @@ fn verify_pdp_ping(context: &SetupContext, sp_index: usize) -> Result<(), Box Result> { // Get dynamically allocated PDP port from context (external port) let port: u16 = context - .get(&format!("curio_sp_{}_pdp_port", sp_index)) + .get(&format!("pdp_sp_{}_pdp_port", sp_index)) .ok_or("Curio PDP port not found in context")? .parse()?; @@ -199,7 +199,7 @@ fn download_piece( ) -> Result, Box> { // Get dynamically allocated PDP port from context let port: u16 = context - .get(&format!("curio_sp_{}_pdp_port", sp_index)) + .get(&format!("pdp_sp_{}_pdp_port", sp_index)) .ok_or("Curio PDP port not found in context")? .parse()?; diff --git a/src/commands/start/pdp_service_provider/pdp_service_provider_step.rs b/src/commands/start/pdp_service_provider/pdp_service_provider_step.rs index beb86945..d6e6e3fc 100644 --- a/src/commands/start/pdp_service_provider/pdp_service_provider_step.rs +++ b/src/commands/start/pdp_service_provider/pdp_service_provider_step.rs @@ -93,7 +93,7 @@ impl Step for PdpSpRegistrationStep { for sp_index in 1..=self.active_sp_count { let pdp_key = format!("pdp_sp_{}_address", sp_index); let eth_key = format!("pdp_sp_{}_eth_address", sp_index); - let port_key = format!("curio_sp_{}_pdp_port", sp_index); + let port_key = format!("pdp_sp_{}_pdp_port", sp_index); let sp_address = context .get(&pdp_key) @@ -169,7 +169,7 @@ impl Step for PdpSpRegistrationStep { for sp_index in 1..=self.active_sp_count { let pdp_key = format!("pdp_sp_{}_address", sp_index); let eth_key = format!("pdp_sp_{}_eth_address", sp_index); - let port_key = format!("curio_sp_{}_pdp_port", sp_index); + let port_key = format!("pdp_sp_{}_pdp_port", sp_index); let sp_address = context .get(&pdp_key) @@ -217,6 +217,12 @@ impl Step for PdpSpRegistrationStep { context, ) { Ok(provider_id) => { + // Store is_approved status in context + context.set( + format!("pdp_sp_{}_is_approved", sp_index), + should_approve.to_string(), + ); + // Only approve if within approved count if should_approve { if let Err(e) = registration::add_to_approved_list( diff --git a/src/external_api/devnet_info.rs b/src/external_api/devnet_info.rs index 71c25de6..1c146396 100644 --- a/src/external_api/devnet_info.rs +++ b/src/external_api/devnet_info.rs @@ -116,6 +116,8 @@ pub struct CurioInfo { pub container_id: String, /// Docker container name pub container_name: String, + /// Whether this provider is approved in FWSS + pub is_approved: bool, /// YugabyteDB information for this provider pub yugabyte: YugabyteInfo, } diff --git a/src/external_api/export.rs b/src/external_api/export.rs index cf1c16fb..8e65e982 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -187,17 +187,22 @@ fn build_single_pdp_service_provider(ctx: &SetupContext, provider_id: u32) -> Op .get(&format!("pdp_sp_{}_address", provider_id)) .unwrap_or_default(); let pdp_port: u16 = ctx - .get(&format!("curio_sp_{}_pdp_port", provider_id)) + .get(&format!("pdp_sp_{}_pdp_port", provider_id)) .and_then(|p| p.parse().ok()) .unwrap_or(4702); let container_id = ctx - .get(&format!("curio_sp_{}_container_id", provider_id)) + .get(&format!("pdp_sp_{}_container_id", provider_id)) .unwrap_or_default(); let container_name = ctx - .get(&format!("curio_sp_{}_container_name", provider_id)) + .get(&format!("pdp_sp_{}_container_name", provider_id)) .unwrap_or_default(); + let is_approved = ctx + .get(&format!("pdp_sp_{}_is_approved", provider_id)) + .and_then(|v| v.parse::().ok()) + .unwrap_or(false); + let yugabyte = build_yugabyte_info(ctx, provider_id); Some(CurioInfo { @@ -207,6 +212,7 @@ fn build_single_pdp_service_provider(ctx: &SetupContext, provider_id: u32) -> Op pdp_service_url: format!("http://localhost:{}", pdp_port), container_id, container_name, + is_approved, yugabyte, }) } From 7ac9deac135c366dd2cc451fbd259c49778d69a3 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 12:37:29 +0530 Subject: [PATCH 19/30] makehappy: fmt --- src/commands/start/curio/daemon.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/commands/start/curio/daemon.rs b/src/commands/start/curio/daemon.rs index 7fd72260..ac6eb2ab 100644 --- a/src/commands/start/curio/daemon.rs +++ b/src/commands/start/curio/daemon.rs @@ -50,10 +50,7 @@ pub fn start_curio_daemon( let container_id = start_curio_container(context, &container_name, docker_args)?; // Store container info in context for export - context.set( - format!("pdp_sp_{}_container_id", sp_index), - container_id, - ); + context.set(format!("pdp_sp_{}_container_id", sp_index), container_id); context.set( format!("pdp_sp_{}_container_name", sp_index), container_name, @@ -88,6 +85,7 @@ fn create_curio_directories(context: &SetupContext, sp_index: usize) -> Result<( /// 1. Container is created but not started /// 2. Networks are connected while container is stopped /// 3. Container is started with Curio as PID 1 (logs work properly) +/// /// Returns the container ID from docker create stdout fn start_curio_container( context: &SetupContext, @@ -117,9 +115,7 @@ fn start_curio_container( } // Extract container ID from stdout - let container_id = String::from_utf8_lossy(&output.stdout) - .trim() - .to_string(); + let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); if container_id.is_empty() { return Err("Docker create did not return container ID".into()); From ff1a4872aa66e09f0ce0c37452dd89ffce1f1f90 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 14:14:21 +0530 Subject: [PATCH 20/30] Update examples/read-devnet-info.js Co-authored-by: Rod Vagg --- examples/read-devnet-info.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/read-devnet-info.js b/examples/read-devnet-info.js index 8edda744..d7ec1ea7 100644 --- a/examples/read-devnet-info.js +++ b/examples/read-devnet-info.js @@ -87,7 +87,6 @@ function printUsers(users) { console.log(` USDFC Balance: ${user.mockusdfc_balance}`); console.log(` Private Key: ${user.private_key_hex.substring(0, 10)}...`); console.log(); - console.log(); } } From 0e50ebea6c84cad9dfdb721b19976c4dba789797 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 14:14:33 +0530 Subject: [PATCH 21/30] Update examples/read-devnet-info.js Co-authored-by: Rod Vagg --- examples/read-devnet-info.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/read-devnet-info.js b/examples/read-devnet-info.js index d7ec1ea7..73c9bcf6 100644 --- a/examples/read-devnet-info.js +++ b/examples/read-devnet-info.js @@ -83,8 +83,6 @@ function printUsers(users) { console.log(`${user.name}:`); console.log(` EVM Address: ${user.evm_addr}`); console.log(` Native Address: ${user.native_addr}`); - console.log(` tFIL Balance: ${user.native_balance_tfil}`); - console.log(` USDFC Balance: ${user.mockusdfc_balance}`); console.log(` Private Key: ${user.private_key_hex.substring(0, 10)}...`); console.log(); } From 2aac53f44a9f8e8a7aa9e4338b4497cd5fa7a725 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 14:15:39 +0530 Subject: [PATCH 22/30] fix: warn message --- src/commands/start/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/start/mod.rs b/src/commands/start/mod.rs index 99205834..4d0afaee 100644 --- a/src/commands/start/mod.rs +++ b/src/commands/start/mod.rs @@ -454,7 +454,7 @@ pub fn start_cluster( Ok(()) } Err(e) => { - warn!("Cluster startup failed, devnet-info.json not exported"); + warn!("Cluster startup failed, external devnet information not exported"); Err(e) } } From 3ebe2eb3f34a395d2ab499735aa0c0624d15da2e Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 14:18:17 +0530 Subject: [PATCH 23/30] fix: use expect() for yugabyte --- src/external_api/export.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/external_api/export.rs b/src/external_api/export.rs index 8e65e982..64f78d74 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -222,17 +222,17 @@ fn build_yugabyte_info(ctx: &SetupContext, provider_id: u32) -> YugabyteInfo { let web_ui_port: u16 = ctx .get(&format!("yugabyte_{}_web_ui_port", provider_id)) .and_then(|p| p.parse().ok()) - .unwrap_or(15433); + .expect(&format!("yugabyte_{}_web_ui_port not found or invalid in context", provider_id)); let master_rpc_port: u16 = ctx .get(&format!("yugabyte_{}_master_rpc_port", provider_id)) .and_then(|p| p.parse().ok()) - .unwrap_or(7100); + .expect(&format!("yugabyte_{}_master_rpc_port not found or invalid in context", provider_id)); let ysql_port: u16 = ctx .get(&format!("yugabyte_{}_ysql_port", provider_id)) .and_then(|p| p.parse().ok()) - .unwrap_or(5433); + .expect(&format!("yugabyte_{}_ysql_port not found or invalid in context", provider_id)); YugabyteInfo { web_ui_url: format!("http://localhost:{}", web_ui_port), From 267779151e1ca3c472c39b2c10bd8adf3338bb76 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 14:22:06 +0530 Subject: [PATCH 24/30] add: expect() --- src/external_api/export.rs | 58 +++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/external_api/export.rs b/src/external_api/export.rs index 64f78d74..2264c3f9 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -42,7 +42,7 @@ fn build_devnet_info(ctx: &SetupContext) -> Result Result Result> { let api_port = ctx .get("lotus_api_port") - .unwrap_or_else(|| "1234".to_string()); + .expect("lotus_api_port not found in context"); Ok(LotusInfo { host_rpc_url: format!("http://localhost:{}/rpc/v1", api_port), container_id: ctx @@ -155,7 +161,7 @@ fn build_lotus_miner_info( let api_port: u16 = ctx .get("lotus_miner_api_port") .and_then(|p| p.parse().ok()) - .unwrap_or(2345); + .expect("lotus_miner_api_port not found or invalid in context"); Ok(LotusMinerInfo { container_id: ctx @@ -173,7 +179,7 @@ fn build_pdp_service_providers(ctx: &SetupContext) -> Vec { let active_count: usize = ctx .get("active_pdp_sp_count") .and_then(|s| s.parse().ok()) - .unwrap_or(1); + .expect("active_pdp_sp_count not found or invalid in context"); (1..=active_count) .filter_map(|id| build_single_pdp_service_provider(ctx, id as u32)) @@ -185,23 +191,38 @@ fn build_single_pdp_service_provider(ctx: &SetupContext, provider_id: u32) -> Op let eth_addr = ctx.get(&format!("pdp_sp_{}_eth_address", provider_id))?; let native_addr = ctx .get(&format!("pdp_sp_{}_address", provider_id)) - .unwrap_or_default(); + .expect(&format!( + "pdp_sp_{}_address not found in context", + provider_id + )); let pdp_port: u16 = ctx .get(&format!("pdp_sp_{}_pdp_port", provider_id)) .and_then(|p| p.parse().ok()) - .unwrap_or(4702); + .expect(&format!( + "pdp_sp_{}_pdp_port not found or invalid in context", + provider_id + )); let container_id = ctx .get(&format!("pdp_sp_{}_container_id", provider_id)) - .unwrap_or_default(); + .expect(&format!( + "pdp_sp_{}_container_id not found in context", + provider_id + )); let container_name = ctx .get(&format!("pdp_sp_{}_container_name", provider_id)) - .unwrap_or_default(); + .expect(&format!( + "pdp_sp_{}_container_name not found in context", + provider_id + )); let is_approved = ctx .get(&format!("pdp_sp_{}_is_approved", provider_id)) .and_then(|v| v.parse::().ok()) - .unwrap_or(false); + .expect(&format!( + "pdp_sp_{}_is_approved not found or invalid in context", + provider_id + )); let yugabyte = build_yugabyte_info(ctx, provider_id); @@ -222,17 +243,26 @@ fn build_yugabyte_info(ctx: &SetupContext, provider_id: u32) -> YugabyteInfo { let web_ui_port: u16 = ctx .get(&format!("yugabyte_{}_web_ui_port", provider_id)) .and_then(|p| p.parse().ok()) - .expect(&format!("yugabyte_{}_web_ui_port not found or invalid in context", provider_id)); + .expect(&format!( + "yugabyte_{}_web_ui_port not found or invalid in context", + provider_id + )); let master_rpc_port: u16 = ctx .get(&format!("yugabyte_{}_master_rpc_port", provider_id)) .and_then(|p| p.parse().ok()) - .expect(&format!("yugabyte_{}_master_rpc_port not found or invalid in context", provider_id)); + .expect(&format!( + "yugabyte_{}_master_rpc_port not found or invalid in context", + provider_id + )); let ysql_port: u16 = ctx .get(&format!("yugabyte_{}_ysql_port", provider_id)) .and_then(|p| p.parse().ok()) - .expect(&format!("yugabyte_{}_ysql_port not found or invalid in context", provider_id)); + .expect(&format!( + "yugabyte_{}_ysql_port not found or invalid in context", + provider_id + )); YugabyteInfo { web_ui_url: format!("http://localhost:{}", web_ui_port), From c970eae3bbd5f1ddf2ed11aa3e66395fe1987d2e Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 14:34:53 +0530 Subject: [PATCH 25/30] fix: export --- src/external_api/export.rs | 75 ++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/src/external_api/export.rs b/src/external_api/export.rs index 2264c3f9..c22d8c56 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -47,7 +47,7 @@ fn build_devnet_info(ctx: &SetupContext) -> Result Vec { +fn build_pdp_service_providers( + ctx: &SetupContext, +) -> Result, Box> { let active_count: usize = ctx .get("active_pdp_sp_count") .and_then(|s| s.parse().ok()) - .expect("active_pdp_sp_count not found or invalid in context"); + .ok_or("active_pdp_sp_count not found or invalid in context")?; (1..=active_count) - .filter_map(|id| build_single_pdp_service_provider(ctx, id as u32)) + .map(|id| build_single_pdp_service_provider(ctx, id as u32)) .collect() } /// Build a single PDP service provider's info. -fn build_single_pdp_service_provider(ctx: &SetupContext, provider_id: u32) -> Option { - let eth_addr = ctx.get(&format!("pdp_sp_{}_eth_address", provider_id))?; +fn build_single_pdp_service_provider( + ctx: &SetupContext, + provider_id: u32, +) -> Result> { + let eth_addr = ctx + .get(&format!("pdp_sp_{}_eth_address", provider_id)) + .ok_or(format!( + "pdp_sp_{}_eth_address not found in context", + provider_id + ))?; let native_addr = ctx .get(&format!("pdp_sp_{}_address", provider_id)) - .expect(&format!( + .ok_or(format!( "pdp_sp_{}_address not found in context", provider_id - )); + ))?; let pdp_port: u16 = ctx .get(&format!("pdp_sp_{}_pdp_port", provider_id)) .and_then(|p| p.parse().ok()) - .expect(&format!( + .ok_or(format!( "pdp_sp_{}_pdp_port not found or invalid in context", provider_id - )); + ))?; let container_id = ctx .get(&format!("pdp_sp_{}_container_id", provider_id)) - .expect(&format!( + .ok_or(format!( "pdp_sp_{}_container_id not found in context", provider_id - )); + ))?; let container_name = ctx .get(&format!("pdp_sp_{}_container_name", provider_id)) - .expect(&format!( + .ok_or(format!( "pdp_sp_{}_container_name not found in context", provider_id - )); + ))?; let is_approved = ctx .get(&format!("pdp_sp_{}_is_approved", provider_id)) .and_then(|v| v.parse::().ok()) - .expect(&format!( + .ok_or(format!( "pdp_sp_{}_is_approved not found or invalid in context", provider_id - )); + ))?; - let yugabyte = build_yugabyte_info(ctx, provider_id); + let yugabyte = build_yugabyte_info(ctx, provider_id)?; - Some(CurioInfo { + Ok(CurioInfo { provider_id, eth_addr, native_addr, @@ -239,36 +249,39 @@ fn build_single_pdp_service_provider(ctx: &SetupContext, provider_id: u32) -> Op } /// Build YugabyteDB info for a provider. -fn build_yugabyte_info(ctx: &SetupContext, provider_id: u32) -> YugabyteInfo { +fn build_yugabyte_info( + ctx: &SetupContext, + provider_id: u32, +) -> Result> { let web_ui_port: u16 = ctx .get(&format!("yugabyte_{}_web_ui_port", provider_id)) .and_then(|p| p.parse().ok()) - .expect(&format!( + .ok_or(format!( "yugabyte_{}_web_ui_port not found or invalid in context", provider_id - )); + ))?; let master_rpc_port: u16 = ctx .get(&format!("yugabyte_{}_master_rpc_port", provider_id)) .and_then(|p| p.parse().ok()) - .expect(&format!( + .ok_or(format!( "yugabyte_{}_master_rpc_port not found or invalid in context", provider_id - )); + ))?; let ysql_port: u16 = ctx .get(&format!("yugabyte_{}_ysql_port", provider_id)) .and_then(|p| p.parse().ok()) - .expect(&format!( + .ok_or(format!( "yugabyte_{}_ysql_port not found or invalid in context", provider_id - )); + ))?; - YugabyteInfo { + Ok(YugabyteInfo { web_ui_url: format!("http://localhost:{}", web_ui_port), master_rpc_port, ysql_port, - } + }) } /// Write a serializable struct to a JSON file. From f79cc5c7be55e128548c5de21d2d5ef86c8a627b Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 14:41:20 +0530 Subject: [PATCH 26/30] fix: examples README --- examples/README.md | 80 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/examples/README.md b/examples/README.md index 162d5451..f4bc0e87 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,12 +6,14 @@ This directory contains examples demonstrating how to interact with a running FO - `read-devnet-info.js` - JavaScript example showing how to read and use the DevNet info - `check-balances.js` - JavaScript example demonstrating how to check user balances +- `devnet-schema.js` - Zod schema for validating DevNet info exports +- `validate-schema.js` - CLI tool for validating devnet-info.json against the schema ## Prerequisites -- Node.js 18+ installed +- Node.js 20+ installed - A running FOC DevNet instance (`foc-devnet start`) -- npm packages: `ethers` (for blockchain interaction) +- npm packages: `ethers`, `zod` ## Usage @@ -22,17 +24,32 @@ This directory contains examples demonstrating how to interact with a running FO 2. Find the devnet-info.json file: ```bash - # The file is located in the run directory, accessible via the latest symlink - cat ~/.foc-devnet/state/latest/devnet-info.json + # The file is located in the run directory + cat ~/.foc-devnet/run//devnet-info.json ``` -3. Run an example: +3. Install dependencies and run examples: ```bash cd examples npm install - node read-devnet-info.js ~/.foc-devnet/state/latest/devnet-info.json + + # Validate the schema + npm run validate-schema ~/.foc-devnet/run//devnet-info.json + + # Read DevNet info + npm run read-info ~/.foc-devnet/run//devnet-info.json + + # Check balances + npm run check-balances ~/.foc-devnet/run//devnet-info.json ``` +## Available Scripts + +- `npm run lint` - Check code for linting issues +- `npm run lint:fix` - Auto-fix linting issues +- `npm run read-info` - Read and display DevNet info +- `npm run check-balances` - Check user account balances + ## DevNet Info Schema (Version 1) The `devnet-info.json` file contains: @@ -41,14 +58,49 @@ The `devnet-info.json` file contains: { "version": 1, "info": { - "run_id": "...", - "start_time": "2026-01-27T...", - "startup_duration": "539.04s", - "users": [...], - "contracts": {...}, - "lotus": {...}, - "lotus_miner": {...}, - "pdp_sps": [...] + "run_id": "20260129T1219_SassyPika", + "start_time": "2026-01-29T06:57:37.473094+00:00", + "startup_duration": "462.21s", + "users": [ + { + "name": "USER_1", + "evm_addr": "0x...", + "native_addr": "t410f...", + "private_key_hex": "0x..." + } + ], + "contracts": { + "multicall3_addr": "0x...", + "mockusdfc_addr": "0x...", + "fwss_service_proxy_addr": "0x...", + ... + }, + "lotus": { + "host_rpc_url": "http://localhost:5701/rpc/v1", + "container_id": "...", + "container_name": "foc-..." + }, + "lotus_miner": { + "container_id": "...", + "container_name": "foc-...", + "api_port": 5703 + }, + "pdp_sps": [ + { + "provider_id": 1, + "eth_addr": "0x...", + "native_addr": "t410f...", + "pdp_service_url": "http://localhost:5714", + "container_id": "...", + "container_name": "foc-...", + "is_approved": true, + "yugabyte": { + "web_ui_url": "http://localhost:5710", + "master_rpc_port": 5706, + "ysql_port": 5704 + } + } + ] } } ``` From 415de9c62eed713ee9275dbf542f85341888e500 Mon Sep 17 00:00:00 2001 From: RedPanda Date: Thu, 29 Jan 2026 17:10:29 +0530 Subject: [PATCH 27/30] remove: eslint --- examples/package.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/package.json b/examples/package.json index 6654d52d..9e3d0550 100644 --- a/examples/package.json +++ b/examples/package.json @@ -5,17 +5,12 @@ "type": "module", "scripts": { "read-info": "node read-devnet-info.js", - "check-balances": "node check-balances.js", - "lint": "eslint *.js", - "lint:fix": "eslint *.js --fix" + "check-balances": "node check-balances.js" }, "dependencies": { "ethers": "^6.0.0", "zod": "^3.22.0" }, - "devDependencies": { - "eslint": "^8.56.0" - }, "engines": { "node": ">=20.0.0" } From cf316421eaf08352b051b0877d4f8083903b8484 Mon Sep 17 00:00:00 2001 From: Steve Loeppky Date: Thu, 29 Jan 2026 10:24:25 -0800 Subject: [PATCH 28/30] docs: enhance README with link to examples Added usage instructions and examples section to README. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index ff98b663..23ecc9c3 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ This will: **That's it!** Your local Filecoin network is running. +### Step 4: Use the Network + +See [examples/README.md](examples/README.md) for how you can easily consume network addresses, parameters, etc. and hook them into Synapse, etc. + --- ## ✨ Key Features @@ -161,6 +165,12 @@ See **[README_ADVANCED.md](README_ADVANCED.md)** for comprehensive documentation --- +## 🚶 Examples + +See [examples/README.md](examples/README.md). + +--- + ## 📝 License MIT License - see [LICENSE](LICENSE) file for details. From 3f5cb819a624d0b410886b7e599579da7b5c32ff Mon Sep 17 00:00:00 2001 From: RedPanda Date: Fri, 30 Jan 2026 11:19:28 +0530 Subject: [PATCH 29/30] feat: linkify README --- examples/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/README.md b/examples/README.md index f4bc0e87..2620e0b7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,10 +4,10 @@ This directory contains examples demonstrating how to interact with a running FO ## Files -- `read-devnet-info.js` - JavaScript example showing how to read and use the DevNet info -- `check-balances.js` - JavaScript example demonstrating how to check user balances -- `devnet-schema.js` - Zod schema for validating DevNet info exports -- `validate-schema.js` - CLI tool for validating devnet-info.json against the schema +- [read-devnet-info.js](read-devnet-info.js) - JavaScript example showing how to read and use the DevNet info +- [check-balances.js](check-balances.js) - JavaScript example demonstrating how to check user balances +- [devnet-schema.js](devnet-schema.js) - Zod schema for validating DevNet info exports +- [validate-schema.js](validate-schema.js) - CLI tool for validating devnet-info.json against the schema ## Prerequisites @@ -105,4 +105,4 @@ The `devnet-info.json` file contains: } ``` -See `read-devnet-info.js` for detailed usage of each field. +See [read-devnet-info.js](read-devnet-info.js) for detailed usage of each field. From 9d9ed2d524a0d09934afca3a695cd2a6e13d28da Mon Sep 17 00:00:00 2001 From: RedPanda Date: Fri, 30 Jan 2026 11:28:40 +0530 Subject: [PATCH 30/30] update: examples --- .github/workflows/ci.yml | 3 +- examples/README.md | 74 +++----------------------------------- examples/check-balances.js | 49 ++++++++++++++++--------- examples/package.json | 4 +-- 4 files changed, 41 insertions(+), 89 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b964123..382612a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,8 @@ jobs: npm install node validate-schema.js "$DEVNET_INFO" node read-devnet-info.js "$DEVNET_INFO" - echo "✓ read-devnet-info.js executed successfully" + node check-balances.js "$DEVNET_INFO" + echo "✓ All examples ran well" # Clean shutdown - name: "EXEC: {Stop cluster}, independent" diff --git a/examples/README.md b/examples/README.md index 2620e0b7..81796bc7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,7 +13,7 @@ This directory contains examples demonstrating how to interact with a running FO - Node.js 20+ installed - A running FOC DevNet instance (`foc-devnet start`) -- npm packages: `ethers`, `zod` +- npm packages: `viem`, `zod` ## Usage @@ -25,7 +25,7 @@ This directory contains examples demonstrating how to interact with a running FO 2. Find the devnet-info.json file: ```bash # The file is located in the run directory - cat ~/.foc-devnet/run//devnet-info.json + cat ~/.foc-devnet/state/latest/devnet-info.json ``` 3. Install dependencies and run examples: @@ -34,75 +34,11 @@ This directory contains examples demonstrating how to interact with a running FO npm install # Validate the schema - npm run validate-schema ~/.foc-devnet/run//devnet-info.json + npm run validate-schema ~/.foc-devnet/state/latest/devnet-info.json # Read DevNet info - npm run read-info ~/.foc-devnet/run//devnet-info.json + npm run read-info ~/.foc-devnet/state/latest/devnet-info.json # Check balances - npm run check-balances ~/.foc-devnet/run//devnet-info.json + npm run check-balances ~/.foc-devnet/state/latest/devnet-info.json ``` - -## Available Scripts - -- `npm run lint` - Check code for linting issues -- `npm run lint:fix` - Auto-fix linting issues -- `npm run read-info` - Read and display DevNet info -- `npm run check-balances` - Check user account balances - -## DevNet Info Schema (Version 1) - -The `devnet-info.json` file contains: - -```json -{ - "version": 1, - "info": { - "run_id": "20260129T1219_SassyPika", - "start_time": "2026-01-29T06:57:37.473094+00:00", - "startup_duration": "462.21s", - "users": [ - { - "name": "USER_1", - "evm_addr": "0x...", - "native_addr": "t410f...", - "private_key_hex": "0x..." - } - ], - "contracts": { - "multicall3_addr": "0x...", - "mockusdfc_addr": "0x...", - "fwss_service_proxy_addr": "0x...", - ... - }, - "lotus": { - "host_rpc_url": "http://localhost:5701/rpc/v1", - "container_id": "...", - "container_name": "foc-..." - }, - "lotus_miner": { - "container_id": "...", - "container_name": "foc-...", - "api_port": 5703 - }, - "pdp_sps": [ - { - "provider_id": 1, - "eth_addr": "0x...", - "native_addr": "t410f...", - "pdp_service_url": "http://localhost:5714", - "container_id": "...", - "container_name": "foc-...", - "is_approved": true, - "yugabyte": { - "web_ui_url": "http://localhost:5710", - "master_rpc_port": 5706, - "ysql_port": 5704 - } - } - ] - } -} -``` - -See [read-devnet-info.js](read-devnet-info.js) for detailed usage of each field. diff --git a/examples/check-balances.js b/examples/check-balances.js index 616e45f1..ac3a3e91 100644 --- a/examples/check-balances.js +++ b/examples/check-balances.js @@ -13,14 +13,15 @@ import { readFileSync, existsSync } from "fs"; import { homedir } from "os"; import { join } from "path"; -import { ethers } from "ethers"; +import { createPublicClient, http, parseAbi } from "viem"; +import { formatUnits } from "viem"; // ERC20 ABI (minimal for balanceOf) -const ERC20_ABI = [ +const ERC20_ABI = parseAbi([ "function balanceOf(address owner) view returns (uint256)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", -]; +]); /** * Load the devnet-info.json file. @@ -42,33 +43,45 @@ function loadDevnetInfo(filePath) { * @returns {string} Formatted amount */ function formatBalance(wei, decimals = 18) { - return ethers.formatUnits(wei, decimals); + return formatUnits(wei, decimals); } /** * Check native FIL balance for an address. - * @param {ethers.Provider} provider - Ethers provider + * @param {object} client - Viem public client * @param {string} address - Address to check * @returns {Promise} Balance in FIL */ -async function checkNativeBalance(provider, address) { - const balance = await provider.getBalance(address); +async function checkNativeBalance(client, address) { + const balance = await client.getBalance({ address }); return formatBalance(balance); } /** * Check ERC20 token balance for an address. - * @param {ethers.Provider} provider - Ethers provider + * @param {object} client - Viem public client * @param {string} tokenAddress - Token contract address * @param {string} userAddress - User address to check * @returns {Promise<{balance: string, symbol: string}>} Balance and symbol */ -async function checkTokenBalance(provider, tokenAddress, userAddress) { - const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider); +async function checkTokenBalance(client, tokenAddress, userAddress) { const [balance, decimals, symbol] = await Promise.all([ - contract.balanceOf(userAddress), - contract.decimals(), - contract.symbol(), + client.readContract({ + address: tokenAddress, + abi: ERC20_ABI, + functionName: "balanceOf", + args: [userAddress], + }), + client.readContract({ + address: tokenAddress, + abi: ERC20_ABI, + functionName: "decimals", + }), + client.readContract({ + address: tokenAddress, + abi: ERC20_ABI, + functionName: "symbol", + }), ]); return { balance: formatBalance(balance, decimals), @@ -94,11 +107,13 @@ async function main() { const { info } = loadDevnetInfo(filePath); // Connect to the Lotus RPC - const provider = new ethers.JsonRpcProvider(info.lotus.host_rpc_url); + const client = createPublicClient({ + transport: http(info.lotus.host_rpc_url), + }); console.log(`Connected to: ${info.lotus.host_rpc_url}`); // Check if network is accessible - const blockNumber = await provider.getBlockNumber(); + const blockNumber = await client.getBlockNumber(); console.log(`Current block: ${blockNumber}\n`); console.log("═══════════════════════════════════════════════════════════"); @@ -110,14 +125,14 @@ async function main() { console.log(`${user.name} (${user.evm_addr}):`); // Check native FIL balance - const filBalance = await checkNativeBalance(provider, user.evm_addr); + const filBalance = await checkNativeBalance(client, user.evm_addr); console.log(` Native FIL: ${filBalance} tFIL`); // Check MockUSDFC balance if (info.contracts.mockusdfc_addr) { try { const { balance, symbol } = await checkTokenBalance( - provider, + client, info.contracts.mockusdfc_addr, user.evm_addr ); diff --git a/examples/package.json b/examples/package.json index 9e3d0550..b2b9b2eb 100644 --- a/examples/package.json +++ b/examples/package.json @@ -8,10 +8,10 @@ "check-balances": "node check-balances.js" }, "dependencies": { - "ethers": "^6.0.0", + "viem": "^2.0.0", "zod": "^3.22.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=24.0.0" } }