use std::io::Read;
use std::str::FromStr;
use crate::decode::{DecodedAccount, Field};
use crate::idl_model::{AccountNode, FieldDef, IdlModel, IdlType, IxDef, SeedDef};
use flate2::bufread::ZlibDecoder;
use serde_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> {
IdlModel::parse(idl)
.errors
.into_iter()
.find(|e| e.code == Some(code))
.map(|e| IdlError {
code,
name: e.name,
msg: e.msg,
})
}
#[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> {
IdlModel::parse(idl)
.instructions
.iter()
.filter_map(|ix| {
Some(IdlInstruction {
name: ix.name.clone()?,
discriminator: ix.discriminator.lossy_bytes()?,
docs: ix.docs.clone(),
accounts: ix.accounts.iter().map(account_spec).collect(),
args: ix
.args
.iter()
.map(|arg| IdlArg {
name: arg.name.clone().unwrap_or_default(),
ty: arg_label(arg.ty.as_ref()),
})
.collect(),
})
})
.collect()
}
fn arg_label(ty: Option<&IdlType>) -> String {
ty.map(IdlType::label).unwrap_or_else(|| "unknown".into())
}
fn account_spec(node: &AccountNode) -> IdlAccountSpec {
IdlAccountSpec {
name: node.name.clone().unwrap_or_default(),
writable: node.writable_modern(),
signer: node.signer_modern(),
pda: node.pda_present,
seeds: node
.seeds
.iter()
.map(|seed| match seed {
SeedDef::Const { bytes } => PdaSeed::Const {
bytes: bytes.clone(),
},
SeedDef::Account { path } => PdaSeed::Account { path: path.clone() },
})
.collect(),
address: node.address.clone(),
}
}
pub(crate) fn find_ix(idl: &Value, data: &[u8]) -> Option<IxDef> {
let disc = data.get(0..8)?;
IdlModel::parse(idl)
.instructions
.into_iter()
.find(|ix| ix.discriminator.lossy_bytes().is_some_and(|b| b == disc))
}
pub(crate) fn decode_ix_args(idl_ix: &IxDef, data: &[u8]) -> Vec<(String, String, String)> {
let mut out = Vec::new();
let mut off = 8usize; for arg in &idl_ix.args {
let name = arg.name.clone().unwrap_or_default();
match arg.ty.as_ref().and_then(resolve_fixed) {
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, arg_label(arg.ty.as_ref()), String::new()));
break;
}
}
}
out
}
#[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: &IdlType) -> Option<Kind> {
match ty {
IdlType::Bool => Some(Kind::Bool),
IdlType::U(n) => Some(Kind::U(*n)),
IdlType::I(n) => Some(Kind::I(*n)),
IdlType::Pubkey { .. } => Some(Kind::Pubkey),
IdlType::Array { inner, len } => {
let inner = resolve_fixed(inner)?;
let count = usize::try_from(*len).ok()?;
let size = inner.size().checked_mul(count)?;
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 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()?))
}
const MAX_WALK_DEPTH: usize = 32;
const MAX_ARRAY_ELEMS: u64 = 1024;
fn walk_fields(
fields: &[FieldDef],
model: &IdlModel,
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 {
let fname = match &f.name {
Some(n) => format!("{prefix}{n}"),
None => return false,
};
let Some(ty) = &f.ty else {
return false;
};
if let IdlType::Defined(tname) = ty {
if let Some(sub) = model.struct_fields(tname) {
if !walk_fields(
sub,
model,
data,
offset,
&format!("{fname}."),
out,
depth + 1,
) {
return false; }
continue;
}
if let Some(variants) = model.enum_variants(tname) {
let Some(&tag) = data.get(*offset) else {
return false;
};
let variant = variants.get(tag as usize);
let vname = variant.and_then(|v| v.name.as_deref()).unwrap_or("unknown");
out.push(Field {
name: fname.clone(),
offset: *offset,
ty: format!("enum {tname}"),
size: 1,
value: vname.to_string(),
editable: false,
note: Some(format!("variant {tag}")),
});
*offset += 1;
if let Some(vfields) = variant.and_then(|v| v.fields.as_ref()) {
let named: Vec<FieldDef> = vfields
.iter()
.enumerate()
.map(|(i, vf)| {
if vf.name_key {
FieldDef {
name: vf.name.clone(),
ty: vf.ty.clone(),
}
} else {
FieldDef {
name: Some(i.to_string()),
ty: Some(vf.whole.clone()),
}
}
})
.collect();
if !walk_fields(
&named,
model,
data,
offset,
&format!("{fname}."),
out,
depth + 1,
) {
return false;
}
}
continue;
}
return false; }
if let IdlType::Array { inner, len } = ty {
if let IdlType::Defined(tname) = inner.as_ref() {
let Some(sub) = model.struct_fields(tname) else {
return false;
};
if *len > MAX_ARRAY_ELEMS {
return false;
}
for i in 0..*len {
if !walk_fields(
sub,
model,
data,
offset,
&format!("{fname}[{i}]."),
out,
depth + 1,
) {
return false;
}
}
continue;
}
}
if matches!(ty, IdlType::Str) {
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 IdlType::Option(inner) = ty {
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 = [FieldDef {
name: Some(fname),
ty: Some((**inner).clone()),
}];
if !walk_fields(&one, model, data, offset, "", out, depth + 1) {
return false;
}
continue;
}
if let IdlType::Vec(inner) = ty {
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 = [FieldDef {
name: Some(format!("{fname}[{i}]")),
ty: Some((**inner).clone()),
}];
if !walk_fields(&one, model, 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_event(idl: &Value, data: &[u8]) -> Option<DecodedAccount> {
use sha2::{Digest, Sha256};
if data.len() < 8 {
return None;
}
let disc = &data[..8];
let events = idl.get("events")?.as_array()?;
let ev = events.iter().find(|e| {
let Some(n) = e.get("name").and_then(|n| n.as_str()) else {
return false;
};
match e.get("discriminator").and_then(|d| d.as_array()) {
Some(arr) => {
let bytes: Vec<u8> = arr
.iter()
.filter_map(|b| b.as_u64())
.map(|b| b as u8)
.collect();
bytes == disc
}
None => Sha256::digest(format!("event:{n}").as_bytes())[..8] == *disc,
}
})?;
let name = ev.get("name")?.as_str()?.to_string();
let model = IdlModel::parse(idl);
let (fields_def, model) = match model.type_def(&name).and_then(|t| t.raw_fields.clone()) {
Some(f) => (f, model),
None => {
let synth = serde_json::json!({
"types": [{ "name": name, "type": { "kind": "struct", "fields": ev.get("fields").cloned().unwrap_or(Value::Array(vec![])) } }]
});
let m2 = IdlModel::parse(&synth);
let f = m2.type_def(&name).and_then(|t| t.raw_fields.clone())?;
(f, m2)
}
};
let mut fields: Vec<Field> = Vec::new();
let mut offset = 8usize;
walk_fields(&fields_def, &model, data, &mut offset, "", &mut fields, 0);
Some(DecodedAccount {
type_name: name,
fields,
})
}
pub(crate) fn ix_arg_span(idl_ix: &IxDef, arg: &str) -> Option<(usize, usize, String)> {
let mut off = 8usize;
for a in &idl_ix.args {
let kind = a.ty.as_ref().and_then(resolve_fixed)?;
if a.name.as_deref() == Some(arg) {
return Some((off, kind.size(), kind.label()));
}
off += kind.size();
}
None
}
pub(crate) fn encode_fixed(label: &str, size: usize, value: &Value) -> Option<Vec<u8>> {
let as_i128 = |v: &Value| -> Option<i128> {
if let Some(n) = v.as_i64() {
return Some(n as i128);
}
if let Some(n) = v.as_u64() {
return Some(n as i128);
}
v.as_str()?.trim().parse::<i128>().ok()
};
Some(match label {
"bool" => vec![u8::from(
value.as_bool().or_else(|| as_i128(value).map(|n| n != 0))?,
)],
"pubkey" => {
let s = value.as_str()?;
Address::from_str(s).ok()?.to_bytes().to_vec()
}
l if l.starts_with('u') => {
let n = as_i128(value)?;
if n < 0 || (size < 16 && n >= (1i128 << (size * 8))) {
return None;
}
(n as u128).to_le_bytes()[..size].to_vec()
}
l if l.starts_with('i') => {
let n = as_i128(value)?;
if size < 16 {
let lim = 1i128 << (size * 8 - 1);
if n < -lim || n >= lim {
return None;
}
}
n.to_le_bytes()[..size].to_vec()
}
_ => return None,
})
}
pub(crate) fn decode_with_idl(idl: &Value, data: &[u8]) -> Option<DecodedAccount> {
if data.len() < 8 {
return None;
}
let disc = &data[0..8];
let model = IdlModel::parse(idl);
let type_name = model
.accounts
.iter()
.find(|a| a.matches(disc))?
.name
.clone()?;
let type_def = model.type_def(&type_name)?;
let fields_def = type_def.raw_fields.as_ref()?;
let mut fields: Vec<Field> = Vec::new();
let mut offset = 8usize;
walk_fields(fields_def, &model, data, &mut offset, "", &mut fields, 0);
Some(DecodedAccount { type_name, fields })
}
#[cfg(test)]
mod tests {
use serde_json::json;
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(&IdlType::parse(&ty)).is_none());
}
}