use std::io::Read;
use crate::decode::{DecodedAccount, Field};
use flate2::bufread::ZlibDecoder;
use serde_json::{json, Value};
use solana_address::Address;
use solana_client::rpc_client::RpcClient;
const PROGRAM_METADATA_PROGRAM: &str = "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S";
pub(crate) fn fetch_idl_json(client: &RpcClient, program_id: Address) -> Option<Value> {
fetch_idl_anchor_account(client, program_id)
.or_else(|| fetch_idl_program_metadata(client, program_id))
}
fn fetch_idl_anchor_account(client: &RpcClient, program_id: Address) -> Option<Value> {
let base = Address::find_program_address(&[], &program_id).0;
let idl_addr = Address::create_with_seed(&base, "anchor:idl", &program_id).ok()?;
let idl_account = client.get_account_data(&idl_addr).ok()?;
let len_bytes: [u8; 4] = idl_account.get(40..44)?.try_into().ok()?;
let len = u32::from_le_bytes(len_bytes) as usize;
let compressed = idl_account.get(44..44 + len)?;
inflate_idl_json(compressed)
}
fn inflate_idl_json(compressed: &[u8]) -> Option<Value> {
let mut out = Vec::new();
ZlibDecoder::new(compressed)
.take(MAX_IDL_JSON)
.read_to_end(&mut out)
.ok()?;
serde_json::from_slice::<Value>(&out).ok()
}
const MAX_IDL_JSON: u64 = 16 * 1024 * 1024;
fn fetch_idl_program_metadata(client: &RpcClient, program_id: Address) -> Option<Value> {
use std::str::FromStr;
let meta = Address::from_str(PROGRAM_METADATA_PROGRAM).ok()?;
let mut seed = b"idl".to_vec();
seed.resize(16, 0); let pda = Address::find_program_address(&[program_id.as_ref(), &seed], &meta).0;
let data = client.get_account_data(&pda).ok()?;
for off in 0..data.len().min(256) {
if data[off] == 0x78 && matches!(data.get(off + 1), Some(0x01 | 0x9c | 0xda)) {
if let Some(v) = inflate_idl_json(&data[off..]) {
return Some(v);
}
}
}
let start = data.iter().position(|&b| b == b'{')?;
serde_json::from_slice::<Value>(&data[start..]).ok()
}
#[derive(serde::Serialize, Clone)]
pub struct IdlError {
pub code: u64,
pub name: String,
pub msg: String,
}
pub(crate) fn error_for_code(idl: &Value, code: u64) -> Option<IdlError> {
idl.get("errors")?.as_array()?.iter().find_map(|e| {
(e.get("code")?.as_u64()? == code).then(|| IdlError {
code,
name: e
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string(),
msg: e
.get("msg")
.and_then(|m| m.as_str())
.unwrap_or("")
.to_string(),
})
})
}
#[derive(serde::Serialize)]
pub struct IdlInstruction {
pub name: String,
pub discriminator: Vec<u8>,
pub docs: Vec<String>,
pub accounts: Vec<IdlAccountSpec>,
pub args: Vec<IdlArg>,
}
#[derive(serde::Serialize)]
pub struct IdlAccountSpec {
pub name: String,
pub writable: bool,
pub signer: bool,
pub pda: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub seeds: Vec<PdaSeed>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
}
#[derive(serde::Serialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum PdaSeed {
Const {
bytes: Vec<u8>,
},
Account {
path: String,
},
}
#[derive(serde::Serialize)]
pub struct IdlArg {
pub name: String,
#[serde(rename = "type")]
pub ty: String,
}
pub fn instructions(idl: &Value) -> Vec<IdlInstruction> {
let Some(list) = idl.get("instructions").and_then(|i| i.as_array()) else {
return vec![];
};
list.iter()
.filter_map(|ix| {
Some(IdlInstruction {
name: ix.get("name")?.as_str()?.to_string(),
discriminator: ix
.get("discriminator")?
.as_array()?
.iter()
.filter_map(|b| b.as_u64().map(|v| v as u8))
.collect(),
docs: ix
.get("docs")
.and_then(|d| d.as_array())
.map(|d| {
d.iter()
.filter_map(|s| s.as_str().map(String::from))
.collect()
})
.unwrap_or_default(),
accounts: ix
.get("accounts")
.and_then(|a| a.as_array())
.map(|a| {
a.iter()
.map(|acc| IdlAccountSpec {
name: acc
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string(),
writable: acc
.get("writable")
.and_then(|w| w.as_bool())
.unwrap_or(false),
signer: acc
.get("signer")
.and_then(|s| s.as_bool())
.unwrap_or(false),
pda: acc.get("pda").is_some(),
seeds: acc
.get("pda")
.and_then(|p| p.get("seeds"))
.and_then(|s| s.as_array())
.map(|seeds| {
seeds
.iter()
.filter_map(|sd| match sd.get("kind")?.as_str()? {
"const" => Some(PdaSeed::Const {
bytes: sd
.get("value")?
.as_array()?
.iter()
.filter_map(|b| b.as_u64().map(|v| v as u8))
.collect(),
}),
"account" => Some(PdaSeed::Account {
path: sd.get("path")?.as_str()?.to_string(),
}),
_ => None,
})
.collect()
})
.unwrap_or_default(),
address: acc
.get("address")
.and_then(|a| a.as_str())
.map(String::from),
})
.collect()
})
.unwrap_or_default(),
args: ix
.get("args")
.and_then(|a| a.as_array())
.map(|a| {
a.iter()
.map(|arg| IdlArg {
name: arg
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string(),
ty: type_label(arg.get("type").unwrap_or(&Value::Null)),
})
.collect()
})
.unwrap_or_default(),
})
})
.collect()
}
pub(crate) fn instruction_by_name<'a>(idl: &'a Value, name: &str) -> Option<&'a Value> {
idl.get("instructions")?
.as_array()?
.iter()
.find(|instruction| instruction.get("name").and_then(Value::as_str) == Some(name))
}
pub(crate) fn find_ix<'a>(idl: &'a Value, data: &[u8]) -> Option<&'a Value> {
let disc = data.get(0..8)?;
idl.get("instructions")?.as_array()?.iter().find(|ix| {
ix.get("discriminator")
.and_then(|d| d.as_array())
.map(|d| {
d.iter()
.filter_map(|b| b.as_u64().map(|v| v as u8))
.collect::<Vec<u8>>()
== disc
})
.unwrap_or(false)
})
}
pub(crate) fn decode_ix_args(idl_ix: &Value, data: &[u8]) -> Vec<(String, String, String)> {
let mut out = Vec::new();
let Some(args) = idl_ix.get("args").and_then(|a| a.as_array()) else {
return out;
};
let mut off = 8usize; for arg in args {
let name = arg
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string();
let ty = arg.get("type").unwrap_or(&Value::Null);
match resolve_fixed(ty) {
Some(kind) => {
let sz = kind.size();
let Some(bytes) = data.get(off..off + sz) else {
break;
};
out.push((name, kind.label(), read_value(bytes, kind)));
off += sz;
}
None => {
out.push((name, type_label(ty), String::new()));
break;
}
}
}
out
}
fn type_label(ty: &Value) -> String {
if let Some(s) = ty.as_str() {
return s.to_string();
}
if let Some(d) = defined_name(ty) {
return d.to_string();
}
if ty.get("vec").is_some() {
return "vec".into();
}
if ty.get("option").is_some() {
return "option".into();
}
if ty.get("array").is_some() {
return "array".into();
}
"unknown".into()
}
#[derive(Clone, Copy)]
enum Kind {
U(usize), I(usize), Bool,
Pubkey,
Bytes(usize), }
impl Kind {
fn label(self) -> String {
match self {
Kind::U(n) => format!("u{}", n * 8),
Kind::I(n) => format!("i{}", n * 8),
Kind::Bool => "bool".into(),
Kind::Pubkey => "pubkey".into(),
Kind::Bytes(n) => format!("[u8; {n}]"),
}
}
fn size(self) -> usize {
match self {
Kind::U(n) | Kind::I(n) | Kind::Bytes(n) => n,
Kind::Bool => 1,
Kind::Pubkey => 32,
}
}
fn editable(self) -> bool {
!matches!(self, Kind::Bytes(_))
}
}
fn resolve_fixed(ty: &Value) -> Option<Kind> {
if let Some(s) = ty.as_str() {
return match s {
"u8" => Some(Kind::U(1)),
"u16" => Some(Kind::U(2)),
"u32" => Some(Kind::U(4)),
"u64" => Some(Kind::U(8)),
"u128" => Some(Kind::U(16)),
"i8" => Some(Kind::I(1)),
"i16" => Some(Kind::I(2)),
"i32" => Some(Kind::I(4)),
"i64" => Some(Kind::I(8)),
"i128" => Some(Kind::I(16)),
"bool" => Some(Kind::Bool),
"pubkey" | "publicKey" => Some(Kind::Pubkey),
_ => None,
};
}
if let Some(arr) = ty.get("array").and_then(|a| a.as_array()) {
let inner = resolve_fixed(arr.first()?)?;
let count = usize::try_from(arr.get(1)?.as_u64()?).ok()?;
let size = inner.size().checked_mul(count)?;
return Some(Kind::Bytes(size));
}
None
}
fn read_value(bytes: &[u8], kind: Kind) -> String {
match kind {
Kind::U(_) => {
let mut buf = [0u8; 16];
buf[..bytes.len()].copy_from_slice(bytes);
u128::from_le_bytes(buf).to_string()
}
Kind::I(n) => {
let mut buf = [0u8; 16];
buf[..bytes.len()].copy_from_slice(bytes);
if bytes[n - 1] & 0x80 != 0 {
for b in &mut buf[n..] {
*b = 0xff;
}
}
i128::from_le_bytes(buf).to_string()
}
Kind::Bool => (bytes[0] != 0).to_string(),
Kind::Pubkey => {
let arr: [u8; 32] = bytes.try_into().unwrap();
Address::from(arr).to_string()
}
Kind::Bytes(_) => bytes.iter().map(|b| format!("{b:02x}")).collect(),
}
}
fn defined_name(ty: &Value) -> Option<&str> {
let d = ty.get("defined")?;
d.as_str()
.or_else(|| d.get("name").and_then(|n| n.as_str()))
}
fn read_u32_at(data: &[u8], offset: usize) -> Option<u32> {
let b = data.get(offset..offset + 4)?;
Some(u32::from_le_bytes(b.try_into().ok()?))
}
fn struct_fields<'a>(types: &'a [Value], name: &str) -> Option<&'a Vec<Value>> {
let t = types
.iter()
.find(|t| t.get("name").and_then(|n| n.as_str()) == Some(name))?;
let ty = t.get("type")?;
if ty.get("kind").and_then(|k| k.as_str()) == Some("struct") {
ty.get("fields")?.as_array()
} else {
None
}
}
fn enum_variants<'a>(types: &'a [Value], name: &str) -> Option<&'a Vec<Value>> {
let t = types
.iter()
.find(|t| t.get("name").and_then(|n| n.as_str()) == Some(name))?;
let ty = t.get("type")?;
if ty.get("kind").and_then(|k| k.as_str()) == Some("enum") {
ty.get("variants")?.as_array()
} else {
None
}
}
const MAX_WALK_DEPTH: usize = 32;
const MAX_ARRAY_ELEMS: u64 = 1024;
fn walk_fields(
fields_json: &[Value],
types: &[Value],
data: &[u8],
offset: &mut usize,
prefix: &str,
out: &mut Vec<Field>,
depth: usize,
) -> bool {
if depth > MAX_WALK_DEPTH {
return false;
}
for f in fields_json {
let fname = match f.get("name").and_then(|n| n.as_str()) {
Some(n) => format!("{prefix}{n}"),
None => return false,
};
let ty = match f.get("type") {
Some(t) => t,
None => return false,
};
if let Some(name) = defined_name(ty) {
if let Some(sub) = struct_fields(types, name) {
if !walk_fields(
sub,
types,
data,
offset,
&format!("{fname}."),
out,
depth + 1,
) {
return false; }
continue;
}
if let Some(variants) = enum_variants(types, name) {
let Some(&tag) = data.get(*offset) else {
return false;
};
let variant = variants.get(tag as usize);
let vname = variant
.and_then(|v| v.get("name"))
.and_then(|n| n.as_str())
.unwrap_or("unknown");
out.push(Field {
name: fname.clone(),
offset: *offset,
ty: format!("enum {name}"),
size: 1,
value: vname.to_string(),
editable: false,
note: Some(format!("variant {tag}")),
});
*offset += 1;
if let Some(vfields) = variant
.and_then(|v| v.get("fields"))
.and_then(|f| f.as_array())
{
let named: Vec<Value> = vfields
.iter()
.enumerate()
.map(|(i, f)| {
if f.get("name").is_some() {
f.clone()
} else {
json!({ "name": i.to_string(), "type": f.clone() })
}
})
.collect();
if !walk_fields(
&named,
types,
data,
offset,
&format!("{fname}."),
out,
depth + 1,
) {
return false;
}
}
continue;
}
return false; }
if let Some(arr) = ty.get("array").and_then(|a| a.as_array()) {
if let (Some(inner), Some(count)) = (arr.first(), arr.get(1).and_then(|c| c.as_u64())) {
if let Some(name) = defined_name(inner) {
let Some(sub) = struct_fields(types, name) else {
return false;
};
if count > MAX_ARRAY_ELEMS {
return false;
}
for i in 0..count {
if !walk_fields(
sub,
types,
data,
offset,
&format!("{fname}[{i}]."),
out,
depth + 1,
) {
return false;
}
}
continue;
}
}
}
if ty.as_str() == Some("string") {
let Some(len) = read_u32_at(data, *offset) else {
return false;
};
let start = *offset + 4;
let end = start + len as usize;
if end > data.len() {
return false;
}
let text = String::from_utf8_lossy(&data[start..end]).to_string();
out.push(Field {
name: fname,
offset: *offset,
ty: "string".into(),
size: 4 + len as usize,
value: text,
editable: false,
note: None,
});
*offset = end;
continue;
}
if let Some(inner) = ty.get("option") {
let Some(&tag) = data.get(*offset) else {
return false;
};
*offset += 1;
if tag == 0 {
out.push(Field {
name: fname,
offset: *offset - 1,
ty: "option".into(),
size: 1,
value: "none".into(),
editable: false,
note: None,
});
continue;
}
let one = json!([{ "name": fname, "type": inner.clone() }]);
let Some(arr) = one.as_array() else {
return false;
};
if !walk_fields(arr, types, data, offset, "", out, depth + 1) {
return false;
}
continue;
}
if let Some(inner) = ty.get("vec") {
let Some(count) = read_u32_at(data, *offset) else {
return false;
};
out.push(Field {
name: format!("{fname}.len"),
offset: *offset,
ty: "u32".into(),
size: 4,
value: count.to_string(),
editable: false,
note: None,
});
*offset += 4;
const MAX_ELEMS: u32 = 32;
if count > MAX_ELEMS {
return false;
}
for i in 0..count {
let one = json!([{ "name": format!("{fname}[{i}]"), "type": inner.clone() }]);
let Some(arr) = one.as_array() else {
return false;
};
if !walk_fields(arr, types, data, offset, "", out, depth + 1) {
return false;
}
}
continue;
}
let kind = match resolve_fixed(ty) {
Some(k) => k,
None => return false, };
let size = kind.size();
if *offset + size > data.len() {
return false;
}
out.push(Field {
name: fname,
offset: *offset,
ty: kind.label(),
size,
value: read_value(&data[*offset..*offset + size], kind),
editable: kind.editable(),
note: None,
});
*offset += size;
}
true
}
pub(crate) fn decode_with_idl(idl: &Value, data: &[u8]) -> Option<DecodedAccount> {
if data.len() < 8 {
return None;
}
let disc = &data[0..8];
let accounts = idl.get("accounts")?.as_array()?;
let mut type_name: Option<String> = None;
for a in accounts {
let d = a.get("discriminator").and_then(|d| d.as_array());
let matches = d.is_some_and(|d| {
d.len() == 8
&& d.iter()
.zip(disc)
.all(|(v, b)| v.as_u64() == Some(*b as u64))
});
if matches {
type_name = a.get("name").and_then(|n| n.as_str()).map(String::from);
break;
}
}
let type_name = type_name?;
let types = idl.get("types")?.as_array()?;
let fields_json = types
.iter()
.find(|t| t.get("name").and_then(|n| n.as_str()) == Some(type_name.as_str()))?
.get("type")?
.get("fields")?
.as_array()?;
let mut fields: Vec<Field> = Vec::new();
let mut offset = 8usize;
walk_fields(fields_json, types, data, &mut offset, "", &mut fields, 0);
Some(DecodedAccount { type_name, fields })
}
#[cfg(test)]
mod tests {
use super::*;
fn account_idl(node_fields: Value, extra_types: Value) -> Value {
let mut types = vec![json!({
"name": "Node",
"type": { "kind": "struct", "fields": node_fields }
})];
if let Some(arr) = extra_types.as_array() {
types.extend(arr.iter().cloned());
}
json!({
"accounts": [{ "name": "Node", "discriminator": [1,2,3,4,5,6,7,8] }],
"types": types,
})
}
#[test]
fn self_referential_idl_type_terminates() {
let idl = account_idl(
json!([{ "name": "next", "type": { "defined": { "name": "Node" } } }]),
json!([]),
);
let mut data = vec![1, 2, 3, 4, 5, 6, 7, 8];
data.resize(8 + 4096, 0);
let _ = decode_with_idl(&idl, &data);
}
#[test]
fn mutually_recursive_idl_types_terminate() {
let idl = account_idl(
json!([{ "name": "b", "type": { "defined": { "name": "B" } } }]),
json!([{
"name": "B",
"type": { "kind": "struct", "fields": [
{ "name": "a", "type": { "defined": { "name": "Node" } } }
] }
}]),
);
let mut data = vec![1, 2, 3, 4, 5, 6, 7, 8];
data.resize(8 + 4096, 0);
let _ = decode_with_idl(&idl, &data);
}
#[test]
fn huge_fixed_array_of_empty_struct_terminates() {
let idl = account_idl(
json!([{
"name": "items",
"type": { "array": [{ "defined": { "name": "Empty" } }, u64::MAX] }
}]),
json!([{
"name": "Empty",
"type": { "kind": "struct", "fields": [] }
}]),
);
let mut data = vec![1, 2, 3, 4, 5, 6, 7, 8];
data.resize(8 + 64, 0);
let _ = decode_with_idl(&idl, &data);
}
#[test]
fn oversized_fixed_array_size_does_not_overflow() {
let ty = json!({ "array": ["u64", u64::MAX] });
assert!(resolve_fixed(&ty).is_none());
}
}