sol-chainsaw 0.0.2

Deserializing Solana accounts using their progam IDL
Documentation
use std::{
    collections::HashMap,
    fmt::Write,
    sync::{Arc, Mutex},
};

use solana_idl::Idl;

use crate::{
    errors::{ChainsawError, ChainsawResult},
    json::{
        JsonIdlTypeDefinitionDeserializer, JsonSerializationOpts,
        JsonTypeDefinitionDeserializerMap,
    },
    utils::{account_discriminator, DiscriminatorBytes},
    IdlProvider,
};

/// Setup to  deserialize accounts for a given program. The accounts are expected to have been
/// serialized using the [borsh] format.
///
/// Uses deserializers defined inside [deserializer] modules under the hood in order to resolve the
/// appropriate [borsh] deserializers for each field.
pub struct JsonAccountsDeserializer<'opts> {
    /// The parsed [Idl] of the program
    pub idl: Idl,

    /// The [IdlProvider] of the program
    pub provider: IdlProvider,

    /// The deserializers for accounts of this program keyed by the discriminator of each account
    /// type.
    pub account_deserializers:
        HashMap<DiscriminatorBytes, JsonIdlTypeDefinitionDeserializer<'opts>>,

    /// Allows looking up a discriminator for an account by name.
    pub account_discriminators: HashMap<String, DiscriminatorBytes>,

    /// Allows looking up a account names by discriminator.
    pub account_names: HashMap<DiscriminatorBytes, String>,

    /// Map of defined type deserializers. Defined types are be nested inside accounts.
    pub type_map: JsonTypeDefinitionDeserializerMap<'opts>,

    /// The [JsonSerializationOpts] specifying how specific data types should be deserialized.
    pub serialization_opts: &'opts JsonSerializationOpts,
}

impl<'opts> JsonAccountsDeserializer<'opts> {
    /// Tries to create an [AccounbtDeserializer] by parsing the [Idl].
    /// Fails if the IDL could not be parsed.
    ///
    /// - [json} the IDL definition in JSON format
    /// - [provider] the provider used to create the IDL
    /// - [serialization_opts] specifying how specific data types should be deserialized.
    pub fn try_from_idl(
        json: &str,
        provider: IdlProvider,
        serialization_opts: &'opts JsonSerializationOpts,
    ) -> ChainsawResult<Self> {
        let idl: Idl = serde_json::from_str(json)?;
        Ok(Self::from_idl(idl, provider, serialization_opts))
    }

    /// Creates an [AccounbtDeserializer] from the provided [Idl]
    /// Fails if the IDL could not be parsed.
    ///
    /// - [idl} the IDL definition
    /// - [provider] the provider used to create the IDL
    /// - [serialization_opts] specifying how specific data types should be deserialized.
    pub fn from_idl(
        idl: Idl,
        provider: IdlProvider,
        serialization_opts: &'opts JsonSerializationOpts,
    ) -> Self {
        let mut account_deserializers = HashMap::new();
        let mut account_discriminators = HashMap::new();
        let type_map = Arc::new(Mutex::new(HashMap::new()));

        for type_definition in &idl.types {
            let instance = JsonIdlTypeDefinitionDeserializer::new(
                type_definition,
                type_map.clone(),
                serialization_opts,
            );
            type_map
                .lock()
                .unwrap()
                .insert(instance.name.clone(), instance);
        }

        for type_definition in &idl.accounts {
            let type_deserializer =
                JsonIdlTypeDefinitionDeserializer::<'opts>::new(
                    type_definition,
                    type_map.clone(),
                    serialization_opts,
                );
            // NOTE: for now we assume that one account doesn't reference another
            //       thus we don't include it in the lookup map for nested types
            //       Similarly for instruction args once we support them
            let discriminator = account_discriminator(&type_definition.name);
            account_deserializers.insert(discriminator, type_deserializer);

            // We expect less accounts to be looked up by name and are fine with an extra
            // lookup instead of keeping another copy of the deserializers by name directly
            account_discriminators
                .insert(type_definition.name.clone(), discriminator);
        }

        let account_names = account_discriminators
            .iter()
            .map(|(name, discriminator)| (*discriminator, name.clone()))
            .collect();

        Self {
            idl,
            provider,
            type_map,
            serialization_opts,
            account_deserializers,
            account_discriminators,
            account_names,
        }
    }

    /// Deserializes an account from the provided data.
    ///
    /// This is the common way of resolving an account json and using it to deserialize
    /// account data.
    /// It expects the first 8 bytes of data to hold the account discriminator as is the case for
    /// anchor accounts.
    /// For all other accounts use [deserialize_account_data_by_name] instead.
    pub fn deserialize_account_data<W: Write>(
        &self,
        account_data: &mut &[u8],
        f: &mut W,
    ) -> ChainsawResult<()> {
        let discriminator = &account_data[..8];
        let deserializer = self
            .account_deserializers
            .get(discriminator)
            .ok_or_else(|| {
                ChainsawError::UnknownDiscriminatedAccount(format!(
                    "disciminator: {:?}",
                    discriminator
                ))
            })?;

        let data = &mut &account_data[8..];
        deserializer.deserialize(f, data)
    }

    /// Deserializes an account from the provided data.
    ///
    /// This method expects account data to **not** be prefixed with 8 bytes of discriminator data.
    /// Instead it derives that discriminator from the provided account name and then looks up the
    /// json.
    pub fn deserialize_account_data_by_name<W: Write>(
        &self,
        account_data: &mut &[u8],
        account_name: &str,
        f: &mut W,
    ) -> ChainsawResult<()> {
        let discriminator = account_discriminator(account_name);
        let deserializer =
            self.account_deserializers.get(&discriminator).ok_or_else(
                || ChainsawError::UnknownAccount(account_name.to_string()),
            )?;

        deserializer.deserialize(f, account_data)
    }

    /// Resolves the account name for the provided discriminator.
    pub fn account_name(
        &self,
        discriminator: &DiscriminatorBytes,
    ) -> Option<&str> {
        self.account_names.get(discriminator).map(|s| s.as_str())
    }
}