use crate::types::{IdlSpec, IdlType, IdlTypeDefKind};
use crate::utils::to_pascal_case;
#[derive(Debug, Clone)]
pub struct PubkeyFieldRef {
pub field_name: String,
pub likely_target: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TypeNode {
pub type_name: String,
pub pubkey_fields: Vec<PubkeyFieldRef>,
}
pub fn extract_type_graph(idl: &IdlSpec) -> Vec<TypeNode> {
let account_names: Vec<&str> = idl.accounts.iter().map(|a| a.name.as_str()).collect();
let mut nodes = Vec::new();
for type_def in &idl.types {
let fields = match &type_def.type_def {
IdlTypeDefKind::Struct { fields, .. } => fields,
_ => continue,
};
let pubkey_fields: Vec<PubkeyFieldRef> = fields
.iter()
.filter(|f| is_pubkey_type(&f.type_))
.map(|f| {
let likely_target = infer_target(&f.name, &account_names);
PubkeyFieldRef {
field_name: f.name.clone(),
likely_target,
}
})
.collect();
if !pubkey_fields.is_empty() {
nodes.push(TypeNode {
type_name: type_def.name.clone(),
pubkey_fields,
});
}
}
nodes
}
fn is_pubkey_type(ty: &IdlType) -> bool {
matches!(ty, IdlType::Simple(s) if s == "pubkey" || s == "publicKey")
}
fn infer_target(field_name: &str, account_names: &[&str]) -> Option<String> {
let candidates = stripped_candidates(field_name);
for candidate in &candidates {
let pascal = to_pascal_case(candidate);
for &acct in account_names {
if acct.eq_ignore_ascii_case(&pascal) {
return Some(acct.to_string());
}
}
}
None
}
fn stripped_candidates(field_name: &str) -> Vec<&str> {
let mut candidates = vec![field_name];
for suffix in &["_id", "_key"] {
if let Some(stripped) = field_name.strip_suffix(suffix) {
if !stripped.is_empty() {
candidates.push(stripped);
}
}
}
candidates
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::parse_idl_file;
use std::path::PathBuf;
fn meteora_fixture() -> IdlSpec {
let path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/meteora_dlmm.json");
parse_idl_file(&path).expect("should parse meteora_dlmm.json")
}
#[test]
fn test_type_graph() {
let idl = meteora_fixture();
let graph = extract_type_graph(&idl);
assert!(
!graph.is_empty(),
"should extract type nodes with pubkey fields"
);
let position = graph.iter().find(|n| n.type_name == "Position");
assert!(
position.is_some(),
"Position type should be in the type graph"
);
let position = position.unwrap();
let lb_pair_field = position
.pubkey_fields
.iter()
.find(|f| f.field_name == "lb_pair");
assert!(
lb_pair_field.is_some(),
"Position should have lb_pair pubkey field"
);
assert_eq!(
lb_pair_field.unwrap().likely_target.as_deref(),
Some("LbPair"),
"lb_pair should resolve to LbPair account type"
);
let owner_field = position
.pubkey_fields
.iter()
.find(|f| f.field_name == "owner");
assert!(
owner_field.is_some(),
"Position should have owner pubkey field"
);
println!("Type graph nodes: {}", graph.len());
for node in &graph {
println!(
" {} — pubkey fields: {:?}",
node.type_name,
node.pubkey_fields
.iter()
.map(|f| format!(
"{} -> {}",
f.field_name,
f.likely_target.as_deref().unwrap_or("?")
))
.collect::<Vec<_>>()
);
}
}
#[test]
fn test_is_pubkey_type() {
assert!(is_pubkey_type(&IdlType::Simple("pubkey".to_string())));
assert!(is_pubkey_type(&IdlType::Simple("publicKey".to_string())));
assert!(!is_pubkey_type(&IdlType::Simple("u64".to_string())));
assert!(!is_pubkey_type(&IdlType::Simple("bool".to_string())));
}
#[test]
fn test_stripped_candidates() {
assert_eq!(stripped_candidates("lb_pair"), vec!["lb_pair"]);
assert_eq!(stripped_candidates("pool_id"), vec!["pool_id", "pool"]);
assert_eq!(stripped_candidates("mint_key"), vec!["mint_key", "mint"]);
assert_eq!(stripped_candidates("_id"), vec!["_id"]);
}
#[test]
fn test_infer_target() {
let accounts = vec!["LbPair", "Position", "BinArray"];
assert_eq!(
infer_target("lb_pair", &accounts),
Some("LbPair".to_string())
);
assert_eq!(
infer_target("position", &accounts),
Some("Position".to_string())
);
assert_eq!(
infer_target("bin_array_id", &accounts),
Some("BinArray".to_string()),
"should match after stripping _id suffix"
);
assert_eq!(
infer_target("unknown_field", &accounts),
None,
"should return None for non-matching fields"
);
}
}