1use alloy_primitives::{Address, U256 as AlloyU256};
2
3use crate::evm::abi::{SpendIntent, SpendSpec};
4use crate::proto::v1;
5
6#[derive(Debug, thiserror::Error)]
13pub enum ProtoConversionError {
14 #[error("Missing field `{field}`")]
16 MissingField { field: &'static str },
17 #[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 #[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 #[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}