sol-chainsaw 0.0.2

Deserializing Solana accounts using their progam IDL
Documentation
// https://github.com/coral-xyz/anchor/blob/27bb6956850477ecbb497dc6c8ff855d97501401/ts/packages/anchor/src/program/index.ts#L335
use borsh::BorshDeserialize;
use flate2::read::ZlibDecoder;
use solana_idl::Idl;
use solana_program::pubkey::Pubkey;
use std::io::Read;

use crate::{
    errors::{ChainsawError, ChainsawResult},
    try_idl_address, IdlProvider,
};
use solana_client::rpc_client::RpcClient;
use solana_sdk::commitment_config::CommitmentConfig;

use super::types::*;

/// Provides functionality to fetch program [IDL]s from chain.
pub struct IdlClient {
    /// The [IdlProvider] of the program
    provider: IdlProvider,

    /// The [RpcClient] used to connect to the RPC provider
    client: RpcClient,
}

impl IdlClient {
    pub fn new(
        provider: IdlProvider,
        cluster: Cluster,
        commitment: CommitmentConfig,
    ) -> Self {
        let client =
            RpcClient::new_with_commitment(cluster.endpoint(), commitment);
        Self { provider, client }
    }

    /// Fetches the IDL for the given program id.
    ///
    /// 1. Derives the IDL account address from the program id
    /// 2. Fetches that account and deserializes it to gain access to authority and data
    /// 3. Extracts the JSON for the IDL from the account data
    /// 4. Deserializes the JSON into an IDL struct
    pub fn fetch_idl(
        &self,
        program_id: Pubkey,
    ) -> ChainsawResult<FetchIdlResult> {
        let (acc, idl_address) = self.fetch_idl_account(&program_id)?;
        let (idl, json) = decode_idl_data(&acc.data)?;
        Ok(FetchIdlResult {
            idl,
            account: acc,
            program_id,
            idl_address,
            json,
        })
    }

    /// Fetches the JSON of the IDL for the given program id.
    /// The same as [Self::fetch_idl] but skips the deserialization step.
    pub fn fetch_idl_json(
        &self,
        program_id: Pubkey,
    ) -> ChainsawResult<FetchIdlJsonResult> {
        let (acc, idl_address) = self.fetch_idl_account(&program_id)?;
        let json = unzip(&acc.data)?;
        Ok(FetchIdlJsonResult {
            account: acc,
            program_id,
            idl_address,
            json,
        })
    }

    /// Fetches the account for the given program id.
    /// The same as [`fetch_idl`] but returns the account data in its raw form.
    pub fn fetch_idl_account(
        &self,
        address: &Pubkey,
    ) -> ChainsawResult<(IdlContainerAccount, Pubkey)> {
        let (data, idl_address) = self.fetch_idl_container_data(address)?;
        let acc = BorshDeserialize::deserialize(&mut data.as_slice()).map_err(
            |err| {
                ChainsawError::IdlContainerAccountDeserializationError(
                    address.to_string(),
                    idl_address.to_string(),
                    err.to_string(),
                )
            },
        )?;

        Ok((acc, idl_address))
    }

    /// Derives the IDL account address from the program id and fetches the raw data for that
    /// account with the discriminator byte stripped for anchor program IDLs.
    pub fn fetch_idl_container_data(
        &self,
        address: &Pubkey,
    ) -> ChainsawResult<(Vec<u8>, Pubkey)> {
        let idl_address = self.idl_address(address)?;
        let account_data = self.client.get_account_data(&idl_address);

        let data = match self.provider {
            IdlProvider::Anchor => {
                let data = account_data?;
                if data.len() < 8 {
                    return Err(
                        ChainsawError::AnchorIdlDataNeedsToBeAtLeast8Bytes,
                    );
                }
                data[8..].to_vec()
            }
            IdlProvider::Shank => account_data?,
        };
        Ok((data, idl_address))
    }

    pub fn idl_address(&self, program_id: &Pubkey) -> ChainsawResult<Pubkey> {
        try_idl_address(&self.provider, program_id)
    }

    pub fn for_anchor_on_mainnet() -> Self {
        Self::new(
            IdlProvider::Anchor,
            Cluster::MainnetBeta,
            CommitmentConfig::processed(),
        )
    }
    pub fn for_anchor_on_devnet() -> Self {
        Self::new(
            IdlProvider::Anchor,
            Cluster::Devnet,
            CommitmentConfig::processed(),
        )
    }
    pub fn for_anchor_on_rpc(url: String) -> Self {
        Self::new(
            IdlProvider::Anchor,
            Cluster::Custom(url),
            CommitmentConfig::processed(),
        )
    }
}

fn decode_idl_data(data: &[u8]) -> ChainsawResult<(Idl, String)> {
    let json = unzip(data)?;
    let idl: Idl = serde_json::from_str(&json)?;
    Ok((idl, json))
}

fn unzip(bytes: &[u8]) -> ChainsawResult<String> {
    let mut zlib = ZlibDecoder::new(bytes);
    let mut write = String::new();
    zlib.read_to_string(&mut write).map_err(|err| {
        ChainsawError::IdlContainerShouldContainZlibData(err.to_string())
    })?;
    Ok(write)
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::*;
    use solana_program::pubkey::Pubkey;

    #[test]
    fn test_idl_address() {
        let consumer = IdlClient::for_anchor_on_devnet();
        let program_id =
            Pubkey::from_str("1USDCmv8QmvZ9JaL7bmevGsNHn7ez8TNahJzCN551sb")
                .unwrap();
        let key = consumer.idl_address(&program_id).unwrap();
        // https://explorer.solana.com/address/Ahs3Spb5rZpBkJPNjRpj285332ZZN4GCfLrvSWZ1z7rE
        assert_eq!(
            key,
            Pubkey::from_str("Ahs3Spb5rZpBkJPNjRpj285332ZZN4GCfLrvSWZ1z7rE")
                .unwrap()
        );
    }

    #[cfg(feature = "test_idl_fetch")]
    #[test]
    fn test_idl_fetch() {
        let consumer = IdlClient::for_anchor_on_mainnet();
        let program_id =
            Pubkey::from_str("1USDCmv8QmvZ9JaL7bmevGsNHn7ez8TNahJzCN551sb")
                .unwrap();
        let FetchIdlResult { idl, .. } =
            consumer.fetch_idl(program_id).unwrap();
        let errors = idl.errors.as_ref().expect("should have errors");

        assert_eq!(errors.len(), 10);
        assert_eq!(idl.instructions.len(), 27);
        assert_eq!(idl.accounts.len(), 6);
        assert_eq!(idl.types.len(), 4);
    }
}