Skip to content

Commit 34b6e84

Browse files
lutterclaude
andcommitted
gnd: Convert command handlers to async to fix nested runtime panic
The main() function uses #[tokio::main] which creates a tokio runtime, but init, add, and build commands were creating their own runtimes and calling block_on(), causing "Cannot start a runtime from within a runtime" errors. Changes: - Convert run_init, run_add, run_build to async functions - Convert helper functions (run_interactive, init_from_contract, get_contract_info, build_subgraph, watch_and_build) to async - Replace std::sync::mpsc with tokio::sync::mpsc in file watcher - Replace std::thread::sleep with tokio::time::sleep - Update main.rs to await the async command handlers - Convert affected tests to #[tokio::test] Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent de706ac commit 34b6e84

File tree

5 files changed

+83
-89
lines changed

5 files changed

+83
-89
lines changed

gnd/src/commands/add.rs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub struct AddOpt {
4949
}
5050

5151
/// Run the add command.
52-
pub fn run_add(opt: AddOpt) -> Result<()> {
52+
pub async fn run_add(opt: AddOpt) -> Result<()> {
5353
// Validate address format first
5454
if !opt.address.starts_with("0x") || opt.address.len() != 42 {
5555
return Err(anyhow!(
@@ -94,7 +94,7 @@ pub fn run_add(opt: AddOpt) -> Result<()> {
9494
.unwrap_or_else(|| "mainnet".to_string());
9595

9696
// Fetch or load ABI
97-
let (abi, contract_name, start_block) = get_contract_info(&opt, &network)?;
97+
let (abi, contract_name, start_block) = get_contract_info(&opt, &network).await?;
9898

9999
// Get project directory
100100
let project_dir = opt.manifest.parent().unwrap_or(Path::new("."));
@@ -143,7 +143,10 @@ pub fn run_add(opt: AddOpt) -> Result<()> {
143143
}
144144

145145
/// Get contract info (ABI, name, start block) from local file or network.
146-
fn get_contract_info(opt: &AddOpt, network: &str) -> Result<(JsonValue, String, Option<u64>)> {
146+
async fn get_contract_info(
147+
opt: &AddOpt,
148+
network: &str,
149+
) -> Result<(JsonValue, String, Option<u64>)> {
147150
if let Some(abi_path) = &opt.abi {
148151
// Load ABI from file
149152
step(
@@ -175,18 +178,14 @@ fn get_contract_info(opt: &AddOpt, network: &str) -> Result<(JsonValue, String,
175178
&format!("Fetching ABI from {} network", network),
176179
);
177180

178-
let runtime = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
179-
180-
let contract_info = runtime.block_on(async {
181-
let service = ContractService::load()
182-
.await
183-
.context("Failed to load contract service")?;
181+
let service = ContractService::load()
182+
.await
183+
.context("Failed to load contract service")?;
184184

185-
service
186-
.get_contract_info(network, &opt.address)
187-
.await
188-
.context("Failed to fetch contract info")
189-
})?;
185+
let contract_info = service
186+
.get_contract_info(network, &opt.address)
187+
.await
188+
.context("Failed to fetch contract info")?;
190189

191190
let contract_name = opt.contract_name.clone().unwrap_or(contract_info.name);
192191

@@ -601,8 +600,8 @@ mod tests {
601600
use super::*;
602601
use crate::scaffold::manifest::{EventInfo, EventInput};
603602

604-
#[test]
605-
fn test_invalid_address() {
603+
#[tokio::test]
604+
async fn test_invalid_address() {
606605
let opt = AddOpt {
607606
address: "invalid".to_string(),
608607
manifest: PathBuf::from("subgraph.yaml"),
@@ -613,7 +612,7 @@ mod tests {
613612
start_block: None,
614613
};
615614

616-
let result = run_add(opt);
615+
let result = run_add(opt).await;
617616
assert!(result.is_err());
618617
assert!(result
619618
.unwrap_err()
@@ -754,8 +753,8 @@ mod tests {
754753
assert!(!mapping.contains("export function handle"));
755754
}
756755

757-
#[test]
758-
fn test_missing_manifest() {
756+
#[tokio::test]
757+
async fn test_missing_manifest() {
759758
let opt = AddOpt {
760759
address: "0x1234567890123456789012345678901234567890".to_string(),
761760
manifest: PathBuf::from("nonexistent.yaml"),
@@ -766,7 +765,7 @@ mod tests {
766765
start_block: None,
767766
};
768767

769-
let result = run_add(opt);
768+
let result = run_add(opt).await;
770769
assert!(result.is_err());
771770
assert!(result.unwrap_err().to_string().contains("Manifest file"));
772771
}

gnd/src/commands/build.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
use std::collections::{HashMap, HashSet};
88
use std::fs;
99
use std::path::{Path, PathBuf};
10-
use std::sync::mpsc;
1110
use std::time::Duration;
1211

12+
use tokio::sync::mpsc;
13+
1314
use anyhow::{anyhow, Context, Result};
1415
use clap::Parser;
1516
use notify::{recommended_watcher, RecursiveMode, Watcher};
@@ -72,7 +73,7 @@ pub struct BuildResult {
7273
}
7374

7475
/// Run the build command.
75-
pub fn run_build(opt: BuildOpt) -> Result<Option<String>> {
76+
pub async fn run_build(opt: BuildOpt) -> Result<Option<String>> {
7677
// Validate output format
7778
if opt.output_format != "wasm" && opt.output_format != "wast" {
7879
return Err(anyhow!(
@@ -82,18 +83,18 @@ pub fn run_build(opt: BuildOpt) -> Result<Option<String>> {
8283
}
8384

8485
if opt.watch {
85-
watch_and_build(&opt)?;
86+
watch_and_build(&opt).await?;
8687
Ok(None)
8788
} else {
88-
let result = build_subgraph(&opt)?;
89+
let result = build_subgraph(&opt).await?;
8990
Ok(result.ipfs_hash)
9091
}
9192
}
9293

9394
/// Watch subgraph files and rebuild on changes.
94-
fn watch_and_build(opt: &BuildOpt) -> Result<()> {
95+
async fn watch_and_build(opt: &BuildOpt) -> Result<()> {
9596
// Do initial build
96-
if let Err(e) = build_subgraph(opt) {
97+
if let Err(e) = build_subgraph(opt).await {
9798
eprintln!("Error during initial build: {}", e);
9899
}
99100

@@ -104,12 +105,13 @@ fn watch_and_build(opt: &BuildOpt) -> Result<()> {
104105
println!("\nWatching subgraph files for changes...");
105106
println!("Press Ctrl+C to stop.\n");
106107

107-
// Set up file watcher
108-
let (tx, rx) = mpsc::channel();
108+
// Set up file watcher with tokio channel
109+
let (tx, mut rx) = mpsc::channel(100);
109110

110111
let mut watcher = recommended_watcher(move |res| {
111112
if let Ok(event) = res {
112-
let _ = tx.send(event);
113+
// Use blocking_send since we're in a sync callback
114+
let _ = tx.blocking_send(event);
113115
}
114116
})
115117
.map_err(|e| anyhow!("Failed to create file watcher: {}", e))?;
@@ -128,8 +130,8 @@ fn watch_and_build(opt: &BuildOpt) -> Result<()> {
128130

129131
// Event loop
130132
loop {
131-
match rx.recv() {
132-
Ok(event) => {
133+
match rx.recv().await {
134+
Some(event) => {
133135
// Check if the event is for a file we care about
134136
let relevant = event.paths.iter().any(|p| {
135137
files_to_watch.iter().any(|f| {
@@ -140,7 +142,7 @@ fn watch_and_build(opt: &BuildOpt) -> Result<()> {
140142

141143
if relevant {
142144
// Debounce: wait a bit and drain any pending events
143-
std::thread::sleep(WATCH_DEBOUNCE);
145+
tokio::time::sleep(WATCH_DEBOUNCE).await;
144146
while rx.try_recv().is_ok() {}
145147

146148
// Get the changed file for logging
@@ -152,12 +154,12 @@ fn watch_and_build(opt: &BuildOpt) -> Result<()> {
152154

153155
println!("\nFile change detected: {}\n", changed);
154156

155-
if let Err(e) = build_subgraph(opt) {
157+
if let Err(e) = build_subgraph(opt).await {
156158
eprintln!("Error during rebuild: {}", e);
157159
}
158160
}
159161
}
160-
Err(_) => {
162+
None => {
161163
return Err(anyhow!("File watcher channel closed"));
162164
}
163165
}
@@ -194,7 +196,7 @@ fn get_files_to_watch(manifest_path: &Path, manifest: &Manifest) -> Vec<PathBuf>
194196
}
195197

196198
/// Build the subgraph.
197-
fn build_subgraph(opt: &BuildOpt) -> Result<BuildResult> {
199+
async fn build_subgraph(opt: &BuildOpt) -> Result<BuildResult> {
198200
// Apply migrations unless skipped
199201
if !opt.skip_migrations {
200202
migrations::apply_migrations(&opt.manifest)?;
@@ -287,9 +289,7 @@ fn build_subgraph(opt: &BuildOpt) -> Result<BuildResult> {
287289

288290
// Upload to IPFS if requested
289291
let ipfs_hash = if let Some(ipfs_url) = &opt.ipfs {
290-
let runtime = tokio::runtime::Runtime::new()
291-
.context("Failed to create async runtime for IPFS upload")?;
292-
let hash = runtime.block_on(upload_to_ipfs(ipfs_url, &opt.output_dir, &manifest))?;
292+
let hash = upload_to_ipfs(ipfs_url, &opt.output_dir, &manifest).await?;
293293
step(Step::Done, &format!("Build completed: {}", hash));
294294
Some(hash)
295295
} else {

gnd/src/commands/deploy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ async fn build_and_upload(opt: &DeployOpt) -> Result<String> {
137137
network_file: opt.network_file.clone(),
138138
};
139139

140-
match run_build(build_opt)? {
140+
match run_build(build_opt).await? {
141141
Some(ipfs_hash) => Ok(ipfs_hash),
142142
None => Err(anyhow!(
143143
"Build succeeded but no IPFS hash was returned. This is unexpected."

gnd/src/commands/init.rs

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ pub struct InitOpt {
112112
}
113113

114114
/// Run the init command.
115-
pub fn run_init(opt: InitOpt) -> Result<()> {
115+
pub async fn run_init(opt: InitOpt) -> Result<()> {
116116
// Check if we need interactive mode
117117
let needs_interactive = should_run_interactive(&opt);
118118

@@ -127,7 +127,7 @@ pub fn run_init(opt: InitOpt) -> Result<()> {
127127
));
128128
}
129129

130-
return run_interactive(opt);
130+
return run_interactive(opt).await;
131131
}
132132

133133
// Non-interactive mode - determine the scaffold source
@@ -143,7 +143,7 @@ pub fn run_init(opt: InitOpt) -> Result<()> {
143143
};
144144

145145
match source {
146-
ScaffoldSource::Contract => init_from_contract(&opt),
146+
ScaffoldSource::Contract => init_from_contract(&opt).await,
147147
ScaffoldSource::Example => init_from_example(&opt),
148148
ScaffoldSource::Subgraph => init_from_subgraph(&opt),
149149
}
@@ -176,12 +176,11 @@ fn should_run_interactive(opt: &InitOpt) -> bool {
176176
}
177177

178178
/// Run in interactive mode.
179-
fn run_interactive(opt: InitOpt) -> Result<()> {
179+
async fn run_interactive(opt: InitOpt) -> Result<()> {
180180
println!("Creating a new subgraph...\n");
181181

182182
// Load the networks registry
183-
let runtime = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
184-
let registry = runtime.block_on(async { NetworksRegistry::load().await })?;
183+
let registry = NetworksRegistry::load().await?;
185184

186185
// Parse start block if provided
187186
let start_block = opt.start_block.as_ref().and_then(|s| s.parse::<u64>().ok());
@@ -229,7 +228,7 @@ fn run_interactive(opt: InitOpt) -> Result<()> {
229228
skip_git: opt.skip_git,
230229
..Default::default()
231230
};
232-
init_from_contract(&contract_opt)
231+
init_from_contract(&contract_opt).await
233232
}
234233
}
235234
}
@@ -241,7 +240,7 @@ enum ScaffoldSource {
241240
}
242241

243242
/// Initialize a subgraph from a contract address.
244-
fn init_from_contract(opt: &InitOpt) -> Result<()> {
243+
async fn init_from_contract(opt: &InitOpt) -> Result<()> {
245244
let address = opt
246245
.from_contract
247246
.as_ref()
@@ -262,48 +261,44 @@ fn init_from_contract(opt: &InitOpt) -> Result<()> {
262261
&format!("Fetching contract info from {} on {}", address, network),
263262
);
264263

265-
// Create async runtime to fetch contract info
266-
let runtime = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
267-
268-
let contract_info = runtime.block_on(async {
264+
// Fetch contract info
265+
let contract_info = if let Some(abi_path) = &opt.abi {
269266
// Load ABI from file if provided
270-
if let Some(abi_path) = &opt.abi {
271-
let abi_str = fs::read_to_string(abi_path)
272-
.with_context(|| format!("Failed to read ABI file: {}", abi_path.display()))?;
273-
let abi: serde_json::Value = serde_json::from_str(&abi_str)
274-
.with_context(|| format!("Failed to parse ABI file: {}", abi_path.display()))?;
275-
276-
// Try to get start block from API if not provided
277-
let start_block = if let Some(block) = &opt.start_block {
278-
block.parse::<u64>().ok()
279-
} else {
280-
// Try to fetch from API
281-
match ContractService::load().await {
282-
Ok(service) => service.get_start_block(network, address).await.ok(),
283-
Err(_) => None,
284-
}
285-
};
286-
287-
Ok::<_, anyhow::Error>(crate::services::ContractInfo {
288-
abi,
289-
name: opt
290-
.contract_name
291-
.clone()
292-
.unwrap_or_else(|| "Contract".to_string()),
293-
start_block,
294-
})
267+
let abi_str = fs::read_to_string(abi_path)
268+
.with_context(|| format!("Failed to read ABI file: {}", abi_path.display()))?;
269+
let abi: serde_json::Value = serde_json::from_str(&abi_str)
270+
.with_context(|| format!("Failed to parse ABI file: {}", abi_path.display()))?;
271+
272+
// Try to get start block from API if not provided
273+
let start_block = if let Some(block) = &opt.start_block {
274+
block.parse::<u64>().ok()
295275
} else {
296-
// Fetch ABI from Etherscan/Sourcify
297-
let service = ContractService::load()
298-
.await
299-
.context("Failed to load contract service")?;
300-
301-
service
302-
.get_contract_info(network, address)
303-
.await
304-
.context("Failed to fetch contract info")
276+
// Try to fetch from API
277+
match ContractService::load().await {
278+
Ok(service) => service.get_start_block(network, address).await.ok(),
279+
Err(_) => None,
280+
}
281+
};
282+
283+
crate::services::ContractInfo {
284+
abi,
285+
name: opt
286+
.contract_name
287+
.clone()
288+
.unwrap_or_else(|| "Contract".to_string()),
289+
start_block,
305290
}
306-
})?;
291+
} else {
292+
// Fetch ABI from Etherscan/Sourcify
293+
let service = ContractService::load()
294+
.await
295+
.context("Failed to load contract service")?;
296+
297+
service
298+
.get_contract_info(network, address)
299+
.await
300+
.context("Failed to fetch contract info")?
301+
};
307302

308303
step(
309304
Step::Done,

gnd/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ async fn main() -> Result<()> {
162162
}
163163
}
164164
Commands::Build(build_opt) => {
165-
if let Err(e) = run_build(build_opt) {
165+
if let Err(e) = run_build(build_opt).await {
166166
eprintln!("Error: {}", e);
167167
std::process::exit(1);
168168
}
@@ -174,13 +174,13 @@ async fn main() -> Result<()> {
174174
}
175175
}
176176
Commands::Init(init_opt) => {
177-
if let Err(e) = run_init(init_opt) {
177+
if let Err(e) = run_init(init_opt).await {
178178
eprintln!("Error: {}", e);
179179
std::process::exit(1);
180180
}
181181
}
182182
Commands::Add(add_opt) => {
183-
if let Err(e) = run_add(add_opt) {
183+
if let Err(e) = run_add(add_opt).await {
184184
eprintln!("Error: {}", e);
185185
std::process::exit(1);
186186
}

0 commit comments

Comments
 (0)