Skip to main content

trident_sdk/
conversions.rs

1use alloy_primitives::{Address, U256 as AlloyU256};
2
3use crate::evm::abi::{SpendIntent, SpendSpec};
4use crate::proto::v1;
5
6/// Errors produced when converting between wire-level proto types and their
7/// native Rust / EVM counterparts.
8///
9/// Raised from [`TryFrom`] impls in this module and surfaces through
10/// [`crate::TridentClientError::ProtoConversion`] and
11/// [`crate::evm::EvmChainError::ProtoConversion`].
12#[derive(Debug, thiserror::Error)]
13pub enum ProtoConversionError {
14    /// A required proto field was `None`.
15    #[error("Missing field `{field}`")]
16    MissingField { field: &'static str },
17    /// An `EvmAddress` proto did not carry exactly 20 bytes.
18    #[error(
19        "Invalid EVM address length for field `{field}`: expected {expected} bytes, got {found}"
20    )]
21    InvalidEvmAddressLength {
22        field: &'static str,
23        expected: usize,
24        found: usize,
25    },
26    /// A `U256` proto did not carry exactly 32 bytes.
27    #[error("Invalid U256 byte length for field `{field}`: expected {expected} bytes, got {found}")]
28    InvalidU256Length {
29        field: &'static str,
30        expected: usize,
31        found: usize,
32    },
33    /// A `Bytes32` proto did not carry exactly 32 bytes.
34    #[error("Invalid Bytes32 length for field `{field}`: expected {expected} bytes, got {found}")]
35    InvalidBytes32Length {
36        field: &'static str,
37        expected: usize,
38        found: usize,
39    },
40}
41
42pub(crate) fn decode_evm_address(
43    value: &v1::EvmAddress,
44    field: &'static str,
45) -> Result<Address, ProtoConversionError> {
46    if value.value.len() != 20 {
47        return Err(ProtoConversionError::InvalidEvmAddressLength {
48            field,
49            expected: 20,
50            found: value.value.len(),
51        });
52    }
53
54    Ok(Address::from_slice(&value.value))
55}
56
57fn decode_u256(value: &v1::U256, field: &'static str) -> Result<AlloyU256, ProtoConversionError> {
58    if value.value.len() != 32 {
59        return Err(ProtoConversionError::InvalidU256Length {
60            field,
61            expected: 32,
62            found: value.value.len(),
63        });
64    }
65
66    Ok(AlloyU256::from_be_slice(&value.value))
67}
68
69impl From<Address> for v1::EvmAddress {
70    fn from(value: Address) -> Self {
71        Self {
72            value: value.as_slice().to_vec(),
73        }
74    }
75}
76
77impl From<Address> for v1::MultiChainAddress {
78    fn from(value: Address) -> Self {
79        Self {
80            chain_address: Some(v1::multi_chain_address::ChainAddress::Evm(value.into())),
81        }
82    }
83}
84
85impl TryFrom<v1::MultiChainAddress> for Address {
86    type Error = ProtoConversionError;
87
88    fn try_from(value: v1::MultiChainAddress) -> Result<Self, Self::Error> {
89        let chain_address = value
90            .chain_address
91            .ok_or(ProtoConversionError::MissingField { field: "address" })?;
92        match chain_address {
93            v1::multi_chain_address::ChainAddress::Evm(evm) => decode_evm_address(&evm, "address"),
94        }
95    }
96}
97
98impl From<AlloyU256> for v1::U256 {
99    fn from(value: AlloyU256) -> Self {
100        Self {
101            value: value.to_be_bytes::<32>().to_vec(),
102        }
103    }
104}
105
106impl TryFrom<v1::U256> for AlloyU256 {
107    type Error = ProtoConversionError;
108
109    fn try_from(value: v1::U256) -> Result<Self, Self::Error> {
110        decode_u256(&value, "value")
111    }
112}
113
114impl From<[u8; 32]> for v1::Bytes32 {
115    fn from(value: [u8; 32]) -> Self {
116        Self {
117            value: value.to_vec(),
118        }
119    }
120}
121
122impl TryFrom<v1::SpendSpec> for SpendSpec {
123    type Error = ProtoConversionError;
124
125    fn try_from(value: v1::SpendSpec) -> Result<Self, Self::Error> {
126        fn addr(
127            field: Option<v1::MultiChainAddress>,
128            name: &'static str,
129        ) -> Result<[u8; 32], ProtoConversionError> {
130            let evm = Address::try_from(
131                field.ok_or(ProtoConversionError::MissingField { field: name })?,
132            )?;
133            let mut out = [0u8; 32];
134            out[12..].copy_from_slice(evm.as_slice());
135            Ok(out)
136        }
137
138        fn u256(
139            field: Option<v1::U256>,
140            name: &'static str,
141        ) -> Result<AlloyU256, ProtoConversionError> {
142            field
143                .ok_or(ProtoConversionError::MissingField { field: name })?
144                .try_into()
145        }
146
147        fn bytes32(
148            field: Option<v1::Bytes32>,
149            name: &'static str,
150        ) -> Result<[u8; 32], ProtoConversionError> {
151            let val = field.ok_or(ProtoConversionError::MissingField { field: name })?;
152            val.value.try_into().map_err(|value: Vec<u8>| {
153                ProtoConversionError::InvalidBytes32Length {
154                    field: name,
155                    expected: 32,
156                    found: value.len(),
157                }
158            })
159        }
160
161        Ok(SpendSpec {
162            srcSpender: addr(value.src_spender, "src_spender")?.into(),
163            srcDomain: value.src_domain,
164            srcToken: addr(value.src_token, "src_token")?.into(),
165            srcDepositContract: addr(value.src_deposit_contract, "src_deposit_contract")?.into(),
166            dstDomain: value.dst_domain,
167            dstToken: addr(value.dst_token, "dst_token")?.into(),
168            dstDeliveryContract: addr(value.dst_delivery_contract, "dst_delivery_contract")?.into(),
169            dstRecipient: addr(value.dst_recipient, "dst_recipient")?.into(),
170            dstCaller: addr(value.dst_caller, "dst_caller")?.into(),
171            amount: u256(value.amount, "amount")?,
172            salt: bytes32(value.salt, "salt")?.into(),
173        })
174    }
175}
176
177impl TryFrom<v1::SpendIntent> for SpendIntent {
178    type Error = ProtoConversionError;
179
180    fn try_from(value: v1::SpendIntent) -> Result<Self, Self::Error> {
181        let spec = value
182            .spec
183            .ok_or(ProtoConversionError::MissingField { field: "spec" })?;
184        let max_fee = value
185            .max_fee
186            .ok_or(ProtoConversionError::MissingField { field: "max_fee" })?
187            .try_into()?;
188
189        Ok(SpendIntent {
190            maxFee: max_fee,
191            expiryHeight: AlloyU256::from(value.expiry_height),
192            spec: spec.try_into()?,
193        })
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    fn sample_proto_intent() -> v1::SpendIntent {
202        let addr = Address::from([0x01; 20]);
203        v1::SpendIntent {
204            spec: Some(v1::SpendSpec {
205                src_spender: Some(addr.into()),
206                src_domain: 1,
207                src_token: Some(Address::from([0x02; 20]).into()),
208                src_deposit_contract: Some(Address::from([0x03; 20]).into()),
209                dst_domain: 2,
210                dst_token: Some(Address::from([0x04; 20]).into()),
211                dst_delivery_contract: Some(Address::from([0x05; 20]).into()),
212                dst_recipient: Some(Address::from([0x06; 20]).into()),
213                dst_caller: Some(Address::from([0x07; 20]).into()),
214                amount: Some(AlloyU256::from(1000u64).into()),
215                salt: Some([0xAA; 32].into()),
216            }),
217            max_fee: Some(AlloyU256::from(10u64).into()),
218            expiry_height: 1000,
219        }
220    }
221
222    #[test]
223    fn decode_spend_intent_success() {
224        let intent = sample_proto_intent();
225        let result: Result<SpendIntent, _> = intent.try_into();
226        assert!(result.is_ok());
227    }
228
229    #[test]
230    fn decode_spend_intent_error_missing_spec() {
231        let mut intent = sample_proto_intent();
232        intent.spec = None;
233        let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
234        assert!(matches!(
235            err,
236            ProtoConversionError::MissingField { field: "spec" }
237        ));
238    }
239
240    #[test]
241    fn decode_spend_intent_error_missing_address() {
242        let mut intent = sample_proto_intent();
243        intent.spec.as_mut().unwrap().src_spender = None;
244        let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
245        assert!(matches!(
246            err,
247            ProtoConversionError::MissingField {
248                field: "src_spender"
249            }
250        ));
251    }
252
253    #[test]
254    fn decode_spend_intent_error_missing_u256() {
255        let mut intent = sample_proto_intent();
256        intent.spec.as_mut().unwrap().amount = None;
257        let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
258        assert!(matches!(
259            err,
260            ProtoConversionError::MissingField { field: "amount" }
261        ));
262    }
263
264    #[test]
265    fn decode_spend_intent_error_missing_bytes32() {
266        let mut intent = sample_proto_intent();
267        intent.spec.as_mut().unwrap().salt = None;
268        let err: ProtoConversionError = SpendIntent::try_from(intent).unwrap_err();
269        assert!(matches!(
270            err,
271            ProtoConversionError::MissingField { field: "salt" }
272        ));
273    }
274}