use crate::types::{IdlPdaSeed, IdlSpec};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeedKind {
Const,
Account,
Arg,
}
#[derive(Debug, Clone)]
pub struct PdaSeedInfo {
pub kind: SeedKind,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct PdaNode {
pub account_name: String,
pub instruction_name: String,
pub seeds: Vec<PdaSeedInfo>,
}
pub fn extract_pda_graph(idl: &IdlSpec) -> Vec<PdaNode> {
let mut nodes = Vec::new();
for ix in &idl.instructions {
for acc in &ix.accounts {
if let Some(pda) = &acc.pda {
let seeds = pda.seeds.iter().map(extract_seed_info).collect();
nodes.push(PdaNode {
account_name: acc.name.clone(),
instruction_name: ix.name.clone(),
seeds,
});
}
}
}
nodes
}
fn extract_seed_info(seed: &IdlPdaSeed) -> PdaSeedInfo {
match seed {
IdlPdaSeed::Const { value } => {
let decoded = String::from_utf8(value.clone()).unwrap_or_else(|_| hex_encode(value));
PdaSeedInfo {
kind: SeedKind::Const,
value: decoded,
}
}
IdlPdaSeed::Account { path, .. } => PdaSeedInfo {
kind: SeedKind::Account,
value: path.clone(),
},
IdlPdaSeed::Arg { path, .. } => PdaSeedInfo {
kind: SeedKind::Arg,
value: path.clone(),
},
}
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::parse_idl_file;
use std::path::PathBuf;
#[test]
fn test_pda_graph() {
let path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/meteora_dlmm.json");
let idl = parse_idl_file(&path).expect("should parse");
let graph = extract_pda_graph(&idl);
assert!(
!graph.is_empty(),
"should extract PDA nodes from meteora_dlmm"
);
let with_seeds = graph.iter().filter(|n| !n.seeds.is_empty()).count();
assert!(with_seeds > 0, "some PDA nodes should have seeds");
let has_account_seed = graph
.iter()
.flat_map(|n| &n.seeds)
.any(|s| s.kind == SeedKind::Account);
let has_arg_seed = graph
.iter()
.flat_map(|n| &n.seeds)
.any(|s| s.kind == SeedKind::Arg);
let has_const_seed = graph
.iter()
.flat_map(|n| &n.seeds)
.any(|s| s.kind == SeedKind::Const);
assert!(has_account_seed, "should have Account seeds");
assert!(has_arg_seed, "should have Arg seeds");
assert!(has_const_seed, "should have Const seeds");
let const_seeds: Vec<&str> = graph
.iter()
.flat_map(|n| &n.seeds)
.filter(|s| s.kind == SeedKind::Const)
.map(|s| s.value.as_str())
.collect();
assert!(
const_seeds
.iter()
.any(|s| s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')),
"at least one const seed should be a readable ASCII string, got: {:?}",
const_seeds
);
println!("PDA graph nodes: {}", graph.len());
println!("Nodes with seeds: {}", with_seeds);
println!(
"Account seeds: {}",
graph
.iter()
.flat_map(|n| &n.seeds)
.filter(|s| s.kind == SeedKind::Account)
.count()
);
println!(
"Arg seeds: {}",
graph
.iter()
.flat_map(|n| &n.seeds)
.filter(|s| s.kind == SeedKind::Arg)
.count()
);
println!(
"Const seeds: {}",
graph
.iter()
.flat_map(|n| &n.seeds)
.filter(|s| s.kind == SeedKind::Const)
.count()
);
println!(
"Sample const values: {:?}",
&const_seeds[..const_seeds.len().min(10)]
);
for node in graph.iter().take(5) {
println!(
" {} in {}: {:?}",
node.account_name,
node.instruction_name,
node.seeds
.iter()
.map(|s| format!("{:?}={}", s.kind, s.value))
.collect::<Vec<_>>()
);
}
}
}