use reqwest::blocking::Client;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
const OSTIUM_PYTHON_SDK_ABI_URL: &str = "https://raw.githubusercontent.com/0xOstium/ostium-python-sdk/main/ostium_python_sdk/abi/abi.py";
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_UPDATE_CONTRACTS");
let force_update = std::env::var("CARGO_FEATURE_UPDATE_CONTRACTS").is_ok();
let abi_dir_exists = Path::new("src/abi/generated").exists();
if force_update {
if let Err(e) = fetch_and_generate_abis() {
println!(
"cargo:warning=Failed to fetch ABIs from Ostium Python SDK: {}",
e
);
println!("cargo:warning=Using existing ABI definitions");
}
} else if !abi_dir_exists {
println!("cargo:warning=ABI directory doesn't exist, attempting initial fetch...");
if let Err(e) = fetch_and_generate_abis() {
println!(
"cargo:warning=Failed to fetch ABIs during initial setup: {}",
e
);
println!("cargo:warning=You may need to run 'cargo build --features update-contracts' to fetch ABIs");
}
}
}
fn fetch_and_generate_abis() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:warning=Fetching latest ABIs from Ostium Python SDK...");
let client = Client::new();
let python_abi_content = client
.get(OSTIUM_PYTHON_SDK_ABI_URL)
.header("User-Agent", "ostium-rust-sdk")
.send()?
.text()?;
let abis = parse_python_abis(&python_abi_content)?;
if abis.is_empty() {
println!("cargo:warning=No ABIs found in the Python SDK");
return Ok(());
}
fs::create_dir_all("src/abi/generated")?;
let mut changes_detected = false;
let mut updated_contracts = Vec::new();
for (contract_name, abi_json) in &abis {
if has_abi_changed(contract_name, abi_json)? {
generate_rust_abi_binding(contract_name, abi_json)?;
changes_detected = true;
updated_contracts.push(contract_name.clone());
}
}
if changes_detected {
generate_abi_mod(&abis)?;
println!(
"cargo:warning=Successfully updated {} ABIs from Python SDK: {}",
updated_contracts.len(),
updated_contracts.join(", ")
);
} else {
println!("cargo:warning=All ABIs are up to date - no changes detected");
}
Ok(())
}
fn parse_python_abis(
python_content: &str,
) -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
let mut abis = HashMap::new();
let lines: Vec<&str> = python_content.lines().collect();
let mut current_abi_name = String::new();
let mut current_abi_content = String::new();
let mut in_abi = false;
let mut bracket_count = 0;
let mut line_number = 0;
for line in lines {
line_number += 1;
let trimmed = line.trim();
if trimmed.ends_with("_abi = [") {
if let Some(name_part) = trimmed.strip_suffix("_abi = [") {
current_abi_name = name_part.trim().to_string();
current_abi_content = String::from("[\n");
in_abi = true;
bracket_count = 1;
println!(
"cargo:warning=Found ABI definition: {} at line {}",
current_abi_name, line_number
);
continue;
}
}
if in_abi {
for ch in trimmed.chars() {
match ch {
'[' | '{' => bracket_count += 1,
']' | '}' => bracket_count -= 1,
_ => {}
}
}
current_abi_content.push_str(line);
current_abi_content.push('\n');
if bracket_count == 0 {
in_abi = false;
match convert_python_to_json(¤t_abi_content) {
Ok(json_abi) => {
let contract_name = if current_abi_name.ends_with("_abi") {
current_abi_name.trim_end_matches("_abi").to_string()
} else {
current_abi_name.clone()
};
println!(
"cargo:warning=Successfully parsed ABI for: {}",
contract_name
);
abis.insert(contract_name, json_abi);
}
Err(e) => {
println!(
"cargo:warning=Failed to parse ABI for {}: {}",
current_abi_name, e
);
println!(
"cargo:warning=ABI content length: {} characters",
current_abi_content.len()
);
}
}
current_abi_name.clear();
current_abi_content.clear();
}
}
}
if in_abi {
println!(
"cargo:warning=Warning: File ended while parsing ABI: {}",
current_abi_name
);
println!("cargo:warning=Bracket count at end: {}", bracket_count);
}
if abis.is_empty() {
println!("cargo:warning=No valid ABIs were parsed from the Python file");
println!("cargo:warning=Total lines processed: {}", line_number);
} else {
let abi_names: Vec<String> = abis.keys().cloned().collect();
println!(
"cargo:warning=Successfully parsed {} ABIs: {}",
abis.len(),
abi_names.join(", ")
);
}
Ok(abis)
}
fn convert_python_to_json(python_abi: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut json_content = python_abi
.replace("True", "true")
.replace("False", "false")
.replace("None", "null");
json_content = json_content
.lines()
.map(|line| {
let line_without_comment = if let Some(comment_pos) = line.find('#') {
let before_comment = &line[..comment_pos];
let quote_count = before_comment.matches('"').count();
if quote_count % 2 == 0 {
before_comment.trim_end().to_string()
} else {
line.to_string()
}
} else {
line.to_string()
};
let trimmed_no_comment = line_without_comment.trim();
if trimmed_no_comment.ends_with(",}") {
line_without_comment.replace(",}", "}")
} else if trimmed_no_comment.ends_with(",]") {
line_without_comment.replace(",]", "]")
} else {
line_without_comment
}
})
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('#')
})
.collect::<Vec<String>>()
.join("\n");
json_content = json_content.trim().to_string();
if let Some(last_bracket_pos) = json_content.rfind(']') {
let after_bracket = &json_content[last_bracket_pos + 1..];
if after_bracket.trim().is_empty() || after_bracket.trim().starts_with('#') {
json_content = json_content[..=last_bracket_pos].to_string();
}
}
match serde_json::from_str::<serde_json::Value>(&json_content) {
Ok(_) => Ok(json_content),
Err(e) => {
println!("cargo:warning=JSON parsing failed: {}", e);
println!(
"cargo:warning=Content length: {} characters",
json_content.len()
);
if let Some(line_col) = extract_line_column_from_error(&e.to_string()) {
let (line_num, col_num) = line_col;
let lines: Vec<&str> = json_content.lines().collect();
if line_num > 0 && line_num <= lines.len() {
println!(
"cargo:warning=Error near line {}: {}",
line_num,
lines[line_num - 1]
);
if col_num > 0 {
let pointer = " ".repeat(col_num.saturating_sub(1)) + "^";
println!("cargo:warning={}", pointer);
}
}
}
Err(format!("Failed to parse ABI as JSON: {}", e).into())
}
}
}
fn extract_line_column_from_error(error_msg: &str) -> Option<(usize, usize)> {
if let Some(line_start) = error_msg.find("line ") {
if let Some(col_start) = error_msg.find(" column ") {
let line_part = &error_msg[line_start + 5..col_start];
let col_part = &error_msg[col_start + 8..];
if let (Ok(line), Ok(col)) = (
line_part.trim().parse::<usize>(),
col_part
.split_whitespace()
.next()
.unwrap_or("0")
.parse::<usize>(),
) {
return Some((line, col));
}
}
}
None
}
fn has_abi_changed(
contract_name: &str,
new_abi_json: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
let output_path = format!("src/abi/generated/{}.rs", contract_name.to_lowercase());
if !Path::new(&output_path).exists() {
return Ok(true);
}
let existing_content = fs::read_to_string(&output_path)?;
if let Some(start_pos) = existing_content.find("r###\"") {
if let Some(end_pos) = existing_content[start_pos + 5..].find("\"###") {
let existing_abi_json = &existing_content[start_pos + 5..start_pos + 5 + end_pos];
match (
serde_json::from_str::<serde_json::Value>(existing_abi_json),
serde_json::from_str::<serde_json::Value>(new_abi_json),
) {
(Ok(existing_abi), Ok(new_abi)) => {
Ok(existing_abi != new_abi)
}
_ => {
Ok(true)
}
}
} else {
Ok(true)
}
} else {
Ok(true)
}
}
fn generate_rust_abi_binding(
contract_name: &str,
abi_json: &str,
) -> Result<(), Box<dyn std::error::Error>> {
println!(
"cargo:warning=Generating ABI binding for: {}",
contract_name
);
let contract_name_upper = contract_name.to_uppercase();
let mut binding = String::new();
binding.push_str(&format!(
"//! Auto-generated ABI binding for {}\n",
contract_name
));
binding.push_str("//! Source: https://github.com/0xOstium/ostium-python-sdk\n\n");
binding.push_str(&format!("/// Raw ABI JSON for {}\n", contract_name));
binding.push_str(&format!(
"pub const {}_ABI: &str = r###\"{}\"###;\n",
contract_name_upper, abi_json
));
let output_path = format!("src/abi/generated/{}.rs", contract_name.to_lowercase());
fs::write(&output_path, binding)?;
Ok(())
}
fn generate_abi_mod(abis: &HashMap<String, String>) -> Result<(), Box<dyn std::error::Error>> {
let mut mod_content =
String::from("//! Auto-generated module for ABIs from Ostium Python SDK\n\n");
for contract_name in abis.keys() {
let module_name = contract_name.to_lowercase();
mod_content.push_str(&format!("pub mod {};\n", module_name));
}
mod_content.push('\n');
for contract_name in abis.keys() {
let module_name = contract_name.to_lowercase();
let const_name = format!("{}_ABI", contract_name.to_uppercase());
mod_content.push_str(&format!("pub use {}::{};\n", module_name, const_name));
}
fs::write("src/abi/generated/mod.rs", mod_content)?;
Ok(())
}