bity-ic-utils 0.3.0

Description spécifique de la crate
Documentation
use candid::Principal;
use icrc_ledger_types::icrc1::account::Account;
use std::str::FromStr;

// Utilities for working with Internet Computer Principal identifiers and accounts.

/// Trait for converting accounts to the principal.subaccount format.
pub trait PrincipalDotAccountFormat {
    /// Converts an account to a string in the format "principal.subaccount"
    fn to_principal_dot_account(&self) -> String;
}

impl PrincipalDotAccountFormat for Account {
    fn to_principal_dot_account(&self) -> String {
        match &self.subaccount {
            Some(subaccount) => format!("{}.{}", self.owner, hex::encode(subaccount)),
            None => format!(
                "{}.0000000000000000000000000000000000000000000000000000000000000000",
                self.owner
            ),
        }
    }
}

/// Converts a string in the format "principal.subaccount" to an Account.
///
/// # Arguments
///
/// * `input` - A string in the format "principal.subaccount"
///
/// # Returns
///
/// Returns a Result containing the parsed Account, or an error message if parsing fails.
pub fn string_to_account(input: String) -> Result<Account, String> {
    if let Some(index) = input.find('.') {
        let (principal_str, subaccount_str) = input.split_at(index);
        let subaccount_str: String = subaccount_str.chars().skip(1).collect();
        match Principal::from_str(principal_str) {
            Ok(valid_principal) => {
                let valid_subaccount_str = if subaccount_str.len() < 64 {
                    format!("{:0>64}", subaccount_str)
                } else {
                    subaccount_str
                };
                match hex::decode(valid_subaccount_str) {
                    Ok(decoded_subaccount) => {
                        if decoded_subaccount.len() == 32 {
                            let mut subaccount_array = [0u8; 32];
                            subaccount_array.copy_from_slice(&decoded_subaccount);

                            let principal_with_subaccount = Account {
                                owner: valid_principal,
                                subaccount: Some(subaccount_array),
                            };
                            Ok(principal_with_subaccount)
                        } else {
                            Err(
                                "split_into_principal_and_account -> subaccount length check, expected 32 bytes".to_string()
                            )
                        }
                    }
                    Err(err) => {
                        let err_message = format!(
                            "split_into_principal_and_account -> hex::decode(subaccount_value){err:?}"
                        );
                        Err(err_message)
                    }
                }
            }
            Err(err) => Err(err.to_string()),
        }
    } else {
        match Principal::from_str(input.as_str()) {
            Ok(valid_principal) => Ok(Account {
                owner: valid_principal,
                subaccount: None,
            }),
            Err(err) => Err(err.to_string()),
        }
    }
}

/// Validates and normalizes a principal.subaccount string.
///
/// # Arguments
///
/// * `input` - A string to validate and normalize
///
/// # Returns
///
/// Returns Some(String) with the normalized format if valid, None if invalid.
pub fn validate_principal_dot_account(input: &str) -> Option<String> {
    match string_to_account(input.to_string()) {
        Ok(account) => Some(account.to_principal_dot_account()),
        Err(_) => match Account::from_str(input) {
            Ok(account) => Some(account.to_principal_dot_account()),
            Err(err) => {
                println!("{err:?}");
                None
            }
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_principal_dot_account_with_checksum() {
        let input =
            "yuijc-oiaaa-aaaap-ahezq-cai-7qfaeci.100000000000000000000000000000000000000000000000000000000000000";
        let expected =
            "yuijc-oiaaa-aaaap-ahezq-cai.0100000000000000000000000000000000000000000000000000000000000000";
        assert_eq!(
            validate_principal_dot_account(input),
            Some(expected.to_string())
        );
    }

    #[test]
    fn test_validate_principal_dot_account_without_checksum() {
        let input =
            "yuijc-oiaaa-aaaap-ahezq-cai.100000000000000000000000000000000000000000000000000000000000000";
        let expected =
            "yuijc-oiaaa-aaaap-ahezq-cai.0100000000000000000000000000000000000000000000000000000000000000";
        assert_eq!(
            validate_principal_dot_account(input),
            Some(expected.to_string())
        );
    }

    #[test]
    fn test_validate_principal_dot_account_invalid_format() {
        let input = "invalid-format";
        assert_eq!(validate_principal_dot_account(input), None);
    }

    #[test]
    fn test_validate_principal_dot_account_short_account_id() {
        let input = "yuijc-oiaaa-aaaap-ahezq-cai.1";
        let expected =
            "yuijc-oiaaa-aaaap-ahezq-cai.0000000000000000000000000000000000000000000000000000000000000001";
        assert_eq!(
            validate_principal_dot_account(input),
            Some(expected.to_string())
        );
    }
}