bobcat-cd 0.10.0

bobcat-sdk utilities for accessing calldata on Arbitrum Stylus.
Documentation
use bobcat_cd::serialisation::SelectorHasher;
use bobcat_cd::{
    EvmCdAddress, EvmCdArray, EvmCdArrayError, EvmCdDeserialise, EvmCdSerialise, EvmCdString,
    const_keccak_sel,
};

#[derive(Debug, PartialEq, Eq, EvmCdSerialise, EvmCdDeserialise)]
enum Call {
    Store(EvmCdAddress, EvmCdArray<u16, 0, 3>),
    SetCount(usize),
}

#[test]
fn selector_hasher_supports_signatures_larger_than_512_bytes() {
    let signature = [b'a'; 600];
    let selector = SelectorHasher::new().update(&signature).selector();

    assert_eq!(selector, const_keccak_sel(&signature));
}

#[test]
fn usize_uses_uint32_abi_encoding() {
    let call = Call::SetCount(u32::MAX as usize);
    let mut encoded = Vec::new();
    call.serialise(&mut encoded).unwrap();

    assert_eq!(&encoded[..4], &const_keccak_sel(b"setCount(uint32)"));
    assert!(encoded[4..32].iter().all(|byte| *byte == 0));
    assert_eq!(&encoded[32..36], &u32::MAX.to_be_bytes());
    assert_eq!(
        Call::deserialise_reader(&mut encoded.as_slice()).unwrap(),
        call
    );

    let mut noncanonical = encoded;
    noncanonical[4] = 1;
    assert!(Call::deserialise_reader(&mut noncanonical.as_slice()).is_err());

    if usize::BITS > 32 {
        let mut output = Vec::new();
        assert!(
            Call::SetCount(u32::MAX as usize + 1)
                .serialise(&mut output)
                .is_err()
        );
    }
}

#[test]
fn address_is_left_padded_and_uses_the_address_abi_type() {
    let address = EvmCdAddress::from([0xabu8; 20]);
    let values = EvmCdArray::try_from_array([7, 9, 0], 2).unwrap();
    let call = Call::Store(address, values);
    let mut encoded = Vec::new();
    call.serialise(&mut encoded).unwrap();

    assert_eq!(&encoded[..4], &const_keccak_sel(b"store(address,uint16[])"));
    assert!(encoded[4..16].iter().all(|byte| *byte == 0));
    assert_eq!(&encoded[16..36], &[0xab; 20]);
    assert_eq!(
        encoded[67], 64,
        "array tail starts after both argument heads"
    );
    assert_eq!(encoded[99], 2);
    assert_eq!(encoded[131], 7);
    assert_eq!(encoded[163], 9);
    assert_eq!(
        Call::deserialise_reader(&mut encoded.as_slice()).unwrap(),
        call
    );
}

#[test]
fn fixed_capacity_dynamic_array_enforces_bounds_without_allocating() {
    type Values = EvmCdArray<u16, 1, 3>;

    assert_eq!(
        Values::try_from_array([0, 0, 0], 0),
        Err(EvmCdArrayError::TooShort),
    );
    assert_eq!(
        Values::try_from_array([0, 0, 0], 4),
        Err(EvmCdArrayError::TooLong),
    );
    assert_eq!(
        EvmCdArray::<u16, 4, 3>::try_from_array([0, 0, 0], 3),
        Err(EvmCdArrayError::InvalidBounds),
    );

    let values = Values::try_from_slice(&[3, 5]).unwrap();
    assert_eq!(values.as_slice(), &[3, 5]);
    assert_eq!(values.len(), 2);
    assert_eq!(values.capacity(), 3);
}

#[test]
fn fixed_capacity_array_supports_dynamic_elements() {
    type Text = EvmCdString<0, 4>;
    type Texts = EvmCdArray<Text, 0, 2>;

    let values = Texts::try_from_array(
        [Text::try_from("a").unwrap(), Text::try_from("bc").unwrap()],
        2,
    )
    .unwrap();
    let mut encoded = Vec::new();
    values.serialise(&mut encoded).unwrap();

    assert_eq!(encoded.len(), 256);
    assert_eq!(encoded[31], 32, "standalone array head points at its tail");
    assert_eq!(encoded[63], 2);
    assert_eq!(
        encoded[95], 64,
        "first string offset follows both element heads"
    );
    assert_eq!(encoded[127], 128);
    assert_eq!(encoded[159], 1);
    assert_eq!(encoded[160], b'a');
    assert_eq!(encoded[223], 2);
    assert_eq!(&encoded[224..226], b"bc");

    let decoded = Texts::deserialise_reader(&mut encoded.as_slice()).unwrap();
    assert_eq!(decoded, values);
}