uip-solana-sdk 0.16.0

Universal Interoperability Protocol Solana SDK
Documentation
#[cfg(feature = "anchor-lang")]
use anchor_lang::{prelude::*, Bumps};
use solana_program::{
    account_info::AccountInfo, program_error::ProgramError, pubkey, pubkey::Pubkey,
};

pub use self::{cpi::*, deser::MessageDataRef};
use self::{deser::DeserializeRef, utils::split_at_checked};

pub mod chains;
mod cpi;
mod deser;
#[cfg(feature = "extension")]
pub mod extension;
mod utils;

#[deprecated(note = "Please use `UipEndpoint::id()` instead")]
pub const UIP_PROGRAM_ID: Pubkey = pubkey!("uipby67GWuDDt1jZTWFdXNrsSu83kcxt9r5CLPTKGhX");

/// Instruction discriminator that should be used when receiving cross-chain
/// messages.
pub const EXECUTE_DISCRIMINATOR: &[u8] = b"execute\0";
const MESSAGE_DISCRIMINATOR: &[u8] = b"message\0";

/// Solana UIP endpoint methods.
pub struct UipEndpoint;

impl UipEndpoint {
    /// Program id of the Solana endpoint.
    pub const fn id() -> Pubkey {
        pubkey!("uipby67GWuDDt1jZTWFdXNrsSu83kcxt9r5CLPTKGhX")
    }
}

#[cfg(feature = "anchor-lang")]
impl Id for UipEndpoint {
    fn id() -> Pubkey {
        Self::id()
    }
}

/// Parses the message account to retrieve [`MessageDataRef`]. It also
/// performs the checks to verify the authenticity of the incoming message.
/// It is still the protocol's responsibility to verify the sender.
pub fn parse_uip_message<'a>(
    message: &'a AccountInfo,
    message_data: &'a [u8],
    expected_dest_addr: &Pubkey,
) -> core::result::Result<MessageDataRef<'a>, ProgramError> {
    if message.owner != &UipEndpoint::id() {
        return Err(ProgramError::InvalidAccountOwner);
    }
    if !message.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let Some(rest) = message_data.strip_prefix(MESSAGE_DISCRIMINATOR) else {
        return Err(ProgramError::InvalidAccountData);
    };

    deserialize_message(rest, expected_dest_addr)
}

fn deserialize_message<'a>(
    data: &'a [u8],
    expected_dest_addr: &Pubkey,
) -> core::result::Result<MessageDataRef<'a>, ProgramError> {
    let data = &mut &data[..];
    let mut read_data = |count| -> core::result::Result<&[u8], ProgramError> {
        let (left, right) =
            split_at_checked(data, count).ok_or(ProgramError::InvalidAccountData)?;
        *data = right;
        Ok(left)
    };

    let _message_status = read_data(1)?;

    let option_tag = read_data(1)?[0];
    if option_tag == 0 {
        return Err(ProgramError::InvalidAccountData);
    }

    let _payer = read_data(32)?;

    let _loaded_at = read_data(8)?;

    let msg_data = MessageDataRef::deserialize_ref(data).ok_or(ProgramError::InvalidAccountData)?;

    if &msg_data.dest_addr != expected_dest_addr {
        return Err(ProgramError::InvalidAccountData);
    }

    Ok(msg_data)
}

/// Generic function to handle account processing and instruction execution.
#[cfg(feature = "anchor-lang")]
pub fn route_instruction<'info, Handler, Accs, InstructionData, HandlerParams>(
    program_id: &Pubkey,
    handler: Handler,
    accounts: &'info [AccountInfo<'info>],
    instruction_data: InstructionData,
    params: HandlerParams,
) -> Result<()>
where
    Handler: FnOnce(Context<Accs>, HandlerParams) -> Result<()>,
    Accs: Accounts<'info, Accs::Bumps> + Bumps + AccountsExit<'info>,
    Accs::Bumps: Default,
    InstructionData: AnchorSerialize,
{
    let mut data = Vec::new();
    instruction_data.serialize(&mut data)?;

    let remaining_accounts = &mut &accounts[..];
    let mut bumps = Default::default();
    let mut accounts = Accs::try_accounts(
        program_id,
        remaining_accounts,
        &data,
        &mut bumps,
        &mut Default::default(),
    )?;
    handler(Context::new(program_id, &mut accounts, remaining_accounts, bumps), params)?;
    accounts.exit(program_id)
}

#[cfg(test)]
mod tests {
    #[cfg(feature = "anchor-lang")]
    use rand::{random, thread_rng, Rng};
    #[cfg(feature = "anchor-lang")]
    use uip_endpoint::state::{
        Message, MessageData, MessageInfo, MessageStatus, Proposal, SrcChainData,
    };

    use super::*;

    #[test]
    fn test_program_id() {
        assert_eq!(UipEndpoint::id(), uip_endpoint::ID_CONST);
    }

    #[test]
    fn test_discriminators() {
        assert_eq!(EXECUTE_DISCRIMINATOR, uip_endpoint::DESTINATION_EXECUTE_DISCRIMINATOR);
    }

    #[test]
    #[cfg(feature = "anchor-lang")]
    fn test_anchor_discriminators() {
        assert_eq!(MESSAGE_DISCRIMINATOR, Message::DISCRIMINATOR);
    }

    #[test]
    #[cfg(feature = "anchor-lang")]
    fn test_deserialize_message() {
        let mut rng = thread_rng();

        for _ in 0..100 {
            let expected_dest_addr = Pubkey::new_from_array(random());

            let expected_total_fee = random();
            let expected_selector = random();
            let expected_payload: Vec<_> = (0..rng.gen_range(0..1024)).map(|_| random()).collect();
            let expected_src_chain_id = random();
            let expected_sender_addr: Vec<_> =
                (0..rng.gen_range(0..32)).map(|_| random()).collect();
            let expected_reserved: Vec<_> = (0..rng.gen_range(0..100)).map(|_| random()).collect();
            let expected_transmitter_params: Vec<_> =
                (0..rng.gen_range(0..100)).map(|_| random()).collect();
            let expected_src_block_number = random();
            let expected_src_op_tx_id = [random(); 2];

            let expected_msg_hash = random();

            let msg = Message {
                status: MessageStatus::Signed,
                info: Some(MessageInfo {
                    payer: Pubkey::new_from_array(random()),
                    hash: expected_msg_hash,
                    loaded_at: random(),
                    msg_data: MessageData {
                        initial_proposal: Proposal {
                            total_fee: expected_total_fee,
                            selector: expected_selector,
                            sender_addr: expected_sender_addr.clone(),
                            dest_addr: expected_dest_addr,
                            payload: expected_payload.clone(),
                            reserved: expected_reserved.clone(),
                            transmitter_params: expected_transmitter_params.clone(),
                        },
                        src_chain_data: SrcChainData {
                            src_chain_id: expected_src_chain_id,
                            src_block_number: expected_src_block_number,
                            src_op_tx_id: expected_src_op_tx_id,
                        },
                    },
                }),
            };

            let data = msg.try_to_vec().unwrap();
            let MessageDataRef {
                payload,
                src_chain_id,
                sender_addr,
                msg_hash,
                total_fee,
                selector,
                reserved,
                dest_addr,
                transmitter_params,
                src_block_number,
                src_op_tx_id,
            } = deserialize_message(&data, &expected_dest_addr).unwrap();
            assert_eq!(payload, expected_payload);
            assert_eq!(src_chain_id, expected_src_chain_id);
            assert_eq!(sender_addr, expected_sender_addr);
            assert_eq!(msg_hash, &expected_msg_hash);
            assert_eq!(total_fee, expected_total_fee);
            assert_eq!(selector, &expected_selector);
            assert_eq!(reserved, expected_reserved);
            assert_eq!(dest_addr, expected_dest_addr);
            assert_eq!(transmitter_params, expected_transmitter_params);
            assert_eq!(src_block_number, expected_src_block_number);
            assert_eq!(src_op_tx_id, &expected_src_op_tx_id);
        }
    }
}