use aptos_sdk::{
Aptos, AptosConfig,
api::response::MoveModuleABI,
codegen::{GeneratorConfig, ModuleGenerator, MoveSourceParser},
};
use clap::{Parser, ValueEnum};
use std::{fs, path::PathBuf};
#[derive(Debug, Clone, ValueEnum)]
enum Network {
Mainnet,
Testnet,
Devnet,
Local,
}
impl Network {
fn to_config(&self) -> AptosConfig {
match self {
Network::Mainnet => AptosConfig::mainnet(),
Network::Testnet => AptosConfig::testnet(),
Network::Devnet => AptosConfig::devnet(),
Network::Local => AptosConfig::local(),
}
}
}
#[derive(Parser, Debug)]
#[command(name = "aptos-codegen")]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long, conflicts_with_all = ["module", "network"])]
input: Option<PathBuf>,
#[arg(short, long, requires = "network")]
module: Option<String>,
#[arg(short, long, value_enum)]
network: Option<Network>,
#[arg(short, long)]
source: Option<PathBuf>,
#[arg(short, long, default_value = ".")]
output: PathBuf,
#[arg(long)]
module_name: Option<String>,
#[arg(long)]
sync: bool,
#[arg(long)]
no_entry_functions: bool,
#[arg(long)]
no_view_functions: bool,
#[arg(long)]
no_structs: bool,
#[arg(long)]
builder: bool,
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
let abi = if let Some(input_path) = &args.input {
println!("Loading ABI from: {}", input_path.display());
let content = fs::read_to_string(input_path)?;
serde_json::from_str::<MoveModuleABI>(&content)?
} else if let Some(module_str) = &args.module {
let network = args.network.as_ref().unwrap();
println!("Fetching module {} from {:?}...", module_str, network);
let config = network.to_config();
let aptos = Aptos::new(config)?;
let parts: Vec<&str> = module_str.split("::").collect();
if parts.len() != 2 {
anyhow::bail!(
"Invalid module format. Expected 'address::module_name', got: {}",
module_str
);
}
let address = aptos_sdk::types::AccountAddress::from_hex(parts[0])?;
let module_name = parts[1];
let response = aptos
.fullnode()
.get_account_module(address, module_name)
.await?;
response
.data
.abi
.ok_or_else(|| anyhow::anyhow!("Module {} does not have ABI information", module_str))?
} else {
anyhow::bail!("Either --input or --module must be specified");
};
println!("Generating code for module: {}::{}", abi.address, abi.name);
let source_info = if let Some(source_path) = &args.source {
println!("Parsing Move source: {}", source_path.display());
let source_content = fs::read_to_string(source_path)?;
let info = MoveSourceParser::parse(&source_content);
println!(
" Found {} functions, {} structs",
info.functions.len(),
info.structs.len()
);
Some(info)
} else {
None
};
let mut config = GeneratorConfig::new()
.with_entry_functions(!args.no_entry_functions)
.with_view_functions(!args.no_view_functions)
.with_structs(!args.no_structs)
.with_async(!args.sync)
.with_builder_pattern(args.builder);
if let Some(name) = &args.module_name {
config = config.with_module_name(name);
}
let mut generator = ModuleGenerator::new(&abi, config);
if let Some(info) = source_info {
generator = generator.with_source_info(info);
}
let code = generator.generate()?;
let output_filename = format!("{}.rs", args.module_name.as_deref().unwrap_or(&abi.name));
let output_path = args.output.join(&output_filename);
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&output_path, &code)?;
println!("Generated: {}", output_path.display());
let entry_count = abi.exposed_functions.iter().filter(|f| f.is_entry).count();
let view_count = abi.exposed_functions.iter().filter(|f| f.is_view).count();
let struct_count = abi.structs.len();
println!();
println!("Summary:");
println!(" - Entry functions: {}", entry_count);
println!(" - View functions: {}", view_count);
println!(" - Structs: {}", struct_count);
if args.source.is_some() {
println!(" - (Parameter names enriched from Move source)");
}
Ok(())
}