trident-sdk 0.2.0

End-user SDK for Trident deposits and cross-chain spends
Documentation
use alloy_primitives::{Address, U256 as AlloyU256};

use crate::evm::abi::{SpendIntent, SpendSpec};
use crate::proto::v1;

/// Errors produced when converting between wire-level proto types and their
/// native Rust / EVM counterparts.
///
/// Raised from [`TryFrom`] impls in this module and surfaces through
/// [`crate::TridentClientError::ProtoConversion`] and
/// [`crate::evm::EvmChainError::ProtoConversion`].
#[derive(Debug, thiserror::Error)]
pub enum ProtoConversionError {
    /// A required proto field was `None`.
    #[error("Missing field `{field}`")]
    MissingField { field: &'static str },
    /// An `EvmAddress` proto did not carry exactly 20 bytes.
    #[error(
        "Invalid EVM address length for field `{field}`: expected {expected} bytes, got {found}"
    )]
    InvalidEvmAddressLength {
        field: &'static str,
        expected: usize,
        found: usize,
    },
    /// A `U256` proto did not carry exactly 32 bytes.
    #[error("Invalid U256 byte length for field `{field}`: expected {expected} bytes, got {found}")]
    InvalidU256Length {
        field: &'static str,
        expected: usize,
        found: usize,
    },
    /// A `Bytes32` proto did not carry exactly 32 bytes.
    #[error("Invalid Bytes32 length for field `{field}`: expected {expected} bytes, got {found}")]
    InvalidBytes32Length {
        field: &'static str,
        expected: usize,
        found: usize,
    },
}

pub(crate) fn decode_evm_address(
    value: &v1::EvmAddress,
    field: &'static str,
) -> Result<Address, ProtoConversionError> {
    if value.value.len() != 20 {
        return Err(ProtoConversionError::InvalidEvmAddressLength {
            field,
            expected: 20,
            found: value.value.len(),
        });
    }

    Ok(Address::from_slice(&value.value))
}

fn decode_u256(value: &v1::U256, field: &'static str) -> Result<AlloyU256, ProtoConversionError> {
    if value.value.len() != 32 {
        return Err(ProtoConversionError::InvalidU256Length {
            field,
            expected: 32,
            found: value.value.len(),
        });
    }

    Ok(AlloyU256::from_be_slice(&value.value))
}

impl From<Address> for v1::EvmAddress {
    fn from(value: Address) -> Self {
        Self {
            value: value.as_slice().to_vec(),
        }
    }
}

impl From<Address> for v1::MultiChainAddress {
    fn from(value: Address) -> Self {
        Self {
            chain_address: Some(v1::multi_chain_address::ChainAddress::Evm(value.into())),
        }
    }
}

impl TryFrom<v1::MultiChainAddress> for Address {
    type Error = ProtoConversionError;

    fn try_from(value: v1::MultiChainAddress) -> Result<Self, Self::Error> {
        let chain_address = value
            .chain_address
            .ok_or(ProtoConversionError::MissingField { field: "address" })?;
        match chain_address {
            v1::multi_chain_address::ChainAddress::Evm(evm) => decode_evm_address(&evm, "address"),
        }
    }
}

impl From<AlloyU256> for v1::U256 {
    fn from(value: AlloyU256) -> Self {
        Self {
            value: value.to_be_bytes::<32>().to_vec(),
        }
    }
}

impl TryFrom<v1::U256> for AlloyU256 {
    type Error = ProtoConversionError;

    fn try_from(value: v1::U256) -> Result<Self, Self::Error> {
        decode_u256(&value, "value")
    }
}

impl From<[u8; 32]> for v1::Bytes32 {
    fn from(value: [u8; 32]) -> Self {
        Self {
            value: value.to_vec(),
        }
    }
}

impl TryFrom<v1::SpendSpec> for SpendSpec {
    type Error = ProtoConversionError;

    fn try_from(value: v1::SpendSpec) -> Result<Self, Self::Error> {
        fn addr(
            field: Option<v1::MultiChainAddress>,
            name: &'static str,
        ) -> Result<[u8; 32], ProtoConversionError> {
            let evm = Address::try_from(
                field.ok_or(ProtoConversionError::MissingField { field: name })?,
            )?;
            let mut out = [0u8; 32];
            out[12..].copy_from_slice(evm.as_slice());
            Ok(out)
        }

        fn u256(
            field: Option<v1::U256>,
            name: &'static str,
        ) -> Result<AlloyU256, ProtoConversionError> {
            field
                .ok_or(ProtoConversionError::MissingField { field: name })?
                .try_into()
        }

        fn bytes32(
            field: Option<v1::Bytes32>,
            name: &'static str,
        ) -> Result<[u8; 32], ProtoConversionError> {
            let val = field.ok_or(ProtoConversionError::MissingField { field: name })?;
            val.value.try_into().map_err(|value: Vec<u8>| {
                ProtoConversionError::InvalidBytes32Length {
                    field: name,
                    expected: 32,
                    found: value.len(),
                }
            })
        }

        Ok(SpendSpec {
            srcSpender: addr(value.src_spender, "src_spender")?.into(),
            srcDomain: value.src_domain,
            srcToken: addr(value.src_token, "src_token")?.into(),
            srcDepositContract: addr(value.src_deposit_contract, "src_deposit_contract")?.into(),
            dstDomain: value.dst_domain,
            dstToken: addr(value.dst_token, "dst_token")?.into(),
            dstDeliveryContract: addr(value.dst_delivery_contract, "dst_delivery_contract")?.into(),
            dstRecipient: addr(value.dst_recipient, "dst_recipient")?.into(),
            dstCaller: addr(value.dst_caller, "dst_caller")?.into(),
            amount: u256(value.amount, "amount")?,
            salt: bytes32(value.salt, "salt")?.into(),
        })
    }
}

impl TryFrom<v1::SpendIntent> for SpendIntent {
    type Error = ProtoConversionError;

    fn try_from(value: v1::SpendIntent) -> Result<Self, Self::Error> {
        let spec = value
            .spec
            .ok_or(ProtoConversionError::MissingField { field: "spec" })?;
        let max_fee = value
            .max_fee
            .ok_or(ProtoConversionError::MissingField { field: "max_fee" })?
            .try_into()?;

        Ok(SpendIntent {
            maxFee: max_fee,
            expiryHeight: AlloyU256::from(value.expiry_height),
            spec: spec.try_into()?,
        })
    }
}

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

    fn sample_proto_intent() -> v1::SpendIntent {
        let addr = Address::from([0x01; 20]);
        v1::SpendIntent {
            spec: Some(v1::SpendSpec {
                src_spender: Some(addr.into()),
                src_domain: 1,
                src_token: Some(Address::from([0x02; 20]).into()),
                src_deposit_contract: Some(Address::from([0x03; 20]).into()),
                dst_domain: 2,
                dst_token: Some(Address::from([0x04; 20]).into()),
                dst_delivery_contract: Some(Address::from([0x05; 20]).into()),
                dst_recipient: Some(Address::from([0x06; 20]).into()),
                dst_caller: Some(Address::from([0x07; 20]).into()),
                amount: Some(AlloyU256::from(1000u64).into()),
                salt: Some([0xAA; 32].into()),
            }),
            max_fee: Some(AlloyU256::from(10u64).into()),
            expiry_height: 1000,
        }
    }

    #[test]
    fn decode_spend_intent_success() {
        let intent = sample_proto_intent();
        let result: Result<SpendIntent, _> = intent.try_into();
        assert!(result.is_ok());
    }

    #[test]
    fn decode_spend_intent_error_missing_spec() {
        let mut intent = sample_proto_intent();
        intent.spec = None;
        let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
        assert!(matches!(
            err,
            ProtoConversionError::MissingField { field: "spec" }
        ));
    }

    #[test]
    fn decode_spend_intent_error_missing_address() {
        let mut intent = sample_proto_intent();
        intent.spec.as_mut().unwrap().src_spender = None;
        let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
        assert!(matches!(
            err,
            ProtoConversionError::MissingField {
                field: "src_spender"
            }
        ));
    }

    #[test]
    fn decode_spend_intent_error_missing_u256() {
        let mut intent = sample_proto_intent();
        intent.spec.as_mut().unwrap().amount = None;
        let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
        assert!(matches!(
            err,
            ProtoConversionError::MissingField { field: "amount" }
        ));
    }

    #[test]
    fn decode_spend_intent_error_missing_bytes32() {
        let mut intent = sample_proto_intent();
        intent.spec.as_mut().unwrap().salt = None;
        let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
        assert!(matches!(
            err,
            ProtoConversionError::MissingField { field: "salt" }
        ));
    }
}